enter current mid-market rate · used only to display CAD beside USD in the tariff table & PDF
—
shows how Country of Export + Canada import are interpreted (e.g. "China → Canada" or "USA → Canada (CUSMA)")
1 USD =
enter current Banxico mid-market rate · used only to display MXN beside USD in the tariff table & PDF
—
shows how Country of Export + Mexico import are interpreted (e.g. "USA → Mexico (USMCA)" or "China → Mexico (MFN + IEPS)")
Sea Freight Method— (See Chart Here)
Incoterms 2010 — Select Method
FOBFree On Board✓
EXWEx Works
FCAFree Carrier
FASFree Alongside Ship
CFRCost & Freight
CIFCost, Insurance & Freight
CPTCarriage Paid To
CIPCarriage & Insurance Paid To
DATDelivered At Terminal
DAPDelivered At Place
DDPDelivered Duty Paid
$
Output will appear here.
Fill in the inputs on the left, then click Calculate Tariffs to see the full breakdown and the orange Download PDF button on this side of the page.
—
—
—
Landed Cost
—
—
—
Total Tariff Cost
—
—
—
Goods Value (no tariffs)
—
—
—
Itemized Active Tariffs — hover name for summary · click for full legal detail
AI Analysis & Notes
⤓Download PDF
Incoterms 2010 — Alibaba Trade Terms Reference
Incoterm Detail
User Guide C8.6
Version C8.6
Version numbering system: Format is X#.# where the letter (A, B, C…) signals a generation — A = original build, B = major architectural overhaul, C = second overhaul, etc. The first number after the letter is the major feature version, the second is the minor/patch version. Example: A1.1 → A1.2 for small updates, A1.2 → A1.3 for more, A1.x → A2.1 for a significant new feature set, A2.x → B1.1 for a full rebuild. The changelog shows the last 5 revisions only — older entries are retired as new ones are added.
Changelog
—
Scroll to see full history ↓
C8.6September 2026Kills the password-revert loop and stops stale-state clobbering on cloud login. (1) RECONCILE NO LONGER DEFAULTS TO "Welcome1". When the C7.7 profile reconcile creates a new local record from a Supabase profiles row, it now starts with an EMPTY password instead of the seed default. This closes the loop where a browser that hadn't yet received the users-store sync would rebuild the account with password=Welcome1 and push that back to app_state, silently overwriting whatever password the Owner had just set from another browser. (2) OWNER PASSWORD OVERRIDE FORCE-PUSHES app_state. After the local usersSave, the password change flow now explicitly upserts the users store into app_state instead of relying on the monkey-patched setItem's async debounce. Guarantees the new password reaches Supabase and every subscribed browser within one round-trip. (3) CLOUD LOGIN PULLS BEFORE SUBSCRIBING. On real Supabase session establishment, _gbCloudSyncStart now fetches every synced key's current row from app_state and applies it locally BEFORE attaching the postgres_changes subscription. Prevents a browser waking up with stale localStorage from clobbering fresh state that other browsers have already pushed — the exact bug that reverted T1's password from Welcome@0568 back to Welcome1. (4) VERSION STAMP. Bumped from C8.5 → C8.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C8.6', AND the JSON export __version__ field now reads 'C8.6'. The CSV download _<VERSION> suffix automatically advances to "_C8.6". All prior changelog rows (C8.5 through B2.8) are preserved.
C8.5September 2026Six deliverables in one ship. (1) NEW admin-delete-user EDGE FUNCTION. When Owner deletes a cloud-connected account, the app now also invokes the Supabase admin-delete-user Edge Function to remove the auth.users row. This fixes the "User already registered" error you got when re-provisioning an account with a previously-deleted email. Delivered as admin-delete-user.ts alongside this build. Deploy on BOTH projects with supabase functions deploy admin-delete-user. The existing SERVICE_ROLE_KEY secret from admin-set-password is reused. (2) GRAVEYARD SORT REVERSED. Newest deletions now appear at the TOP of the Account Graveyard list, oldest at the bottom. (3) TARIFF NEWS SCROLLER +10% FONT. Headlines in the red BREAKING ticker bumped from 12px to 13.2px so they read easier without changing the ticker height. (4) BUILD NOTES + LEADS CROSS-BROWSER SYNC. Added gbBizCalcBuildNotesTabs_v1 and gbBizCalcLeadsTabs_v1 to the C6.8/C8.3 app_state sync set. Notes typed in one browser now propagate to every other browser signed in as the same account within a second. (HISTORICAL NOTE ON RESTORING OLD MG-SIDE NOTES: the notes were stored ONLY in localStorage, never baked into the HTML file, so no earlier version of the file contains them. If your Safari or Chrome still has them in Application → Local Storage → gbBizCalcBuildNotesTabs_v1 / gbBizCalcLeadsTabs_v1, paste them back into the MG tab and they will now sync everywhere.) (5) API KEY VISIBILITY + USAGE PERMISSIONS. Two new Owner-only pills per user: Can see API key (default OFF — hides the "🔑 API Key: not set" readout from that user), and Can use API key functions (default OFF — blocks Update Tariffs + Update News for that user). Owner always sees the API key and always has full access. (6) API KEY RATE LIMIT. Owner can set a per-user usage cap on API-key-consuming functions: N uses per day / week / month / unlimited. Enforced on Update Tariffs and Update News. Current usage shown next to the limit in the Users tab. (7) VERSION STAMP. Bumped from C8.4 → C8.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C8.5', AND the JSON export __version__ field now reads 'C8.5'. The CSV download _<VERSION> suffix automatically advances to "_C8.5". All prior changelog rows (C8.4 through B2.8) are preserved.
C8.4May 2026Root-cause fix for same-account cross-browser sync. (1) SELF-WRITE FILTER WAS USER-UID-BASED. The C6.8 app_state Realtime handler skipped events whose updated_by matched the current user's UID — which meant when Safari and Chrome were BOTH signed in as Owner, they silently filtered out each other's writes as "my own change." That's why local-only Test Accounts, Customer Card edits, and any other app_state-synced data didn't propagate between same-account browsers. (2) NEW PER-BROWSER SESSION ID. Every browser tab now generates a random session ID at page load (GB_SYNC_SESSION_ID). Every push to app_state wraps the payload as { _sid: sessionId, _payload: originalValue }. On receive, the browser skips only events whose _sid matches its OWN session ID — not the user UID. Different browsers get different session IDs even when signed in as the same account, so their writes now propagate. (3) BACKWARD COMPAT. Legacy app_state rows (without the _sid/_payload wrapper) apply as-is so nothing pre-C8.4 breaks. (4) VERSION STAMP. Bumped from C8.3 → C8.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C8.4', AND the JSON export __version__ field now reads 'C8.4'. The CSV download _<VERSION> suffix automatically advances to "_C8.4". All prior changelog rows (C8.3 through B2.8) are preserved.
C8.3May 2026Local-only accounts now cross browsers. (1) USERS STORE IS SYNCED. Adding gbBizCalcUsers_v1 to the C6.8 Realtime app_state sync set means every browser can see accounts created on any other browser — including LOCAL-ONLY Test Accounts that never had a cloud identity. (2) ACTIVE USER ID PRESERVED PER BROWSER. When the users store arrives as a remote update, the receiving browser keeps its OWN activeUserId intact so the sender can't sign the receiver out or switch their active account. The remote users array replaces the local one; only activeUserId is preserved locally. (3) GRAVEYARD, TITLE-PERMISSIONS, CUSTOM-TITLES ALSO SYNC. Because these live inside the same users store, they now propagate too — delete a user in Safari and Chrome sees them appear in the graveyard within a second. (4) VERSION STAMP. Bumped from C8.2 → C8.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C8.3', AND the JSON export __version__ field now reads 'C8.3'. The CSV download _<VERSION> suffix automatically advances to "_C8.3". All prior changelog rows (C8.2 through B2.8) are preserved.
C8.2May 2026Reconcile prune was too aggressive. (1) C8.1'S PRUNE WIPED LOCAL-ONLY TEST ACCOUNTS. If you created a Test Account without turning Connect to Supabase on, there is no public.profiles row for that account by design (it's a local-only scratch account). C8.1's prune saw the missing profile row and deleted the local record on the next Owner login. C8.2 fixes the filter: only accounts that HAVE a cloud identity (cloudConnected: true OR supaUid is set) but whose handle is missing from the cloud list get pruned. Local-only accounts (no cloud identity by design) are preserved untouched. Owner is still always preserved (belt-and-suspenders). (2) VERSION STAMP. Bumped from C8.1 → C8.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C8.2', AND the JSON export __version__ field now reads 'C8.2'. The CSV download _<VERSION> suffix automatically advances to "_C8.2". All prior changelog rows (C8.1 through B2.8) are preserved.
C8.1May 2026Chrome ghost-accounts fix. (1) RECONCILE NOW PRUNES LOCAL USERS NOT IN CLOUD. C7.7 added missing users from Supabase but never deleted the local records of users that were deleted on another browser. That is why Chrome kept showing Jason and Sand Box in the login dropdown after Safari had wiped them. C8.1 extends _gbReconcileUsersFromProfiles: after adding any missing users, it also loops over local non-Owner accounts. Any local user whose handle does not appear in the fetched profiles list is removed from the local users store. Owner is always preserved (fail-safe). (2) SAFEGUARD FOR OFFLINE / EMPTY-CLOUD STATES. If the profiles fetch fails or returns zero rows (e.g. the network dropped between fetch and process), the prune step is skipped entirely so a temporary Supabase outage can never wipe local accounts. (3) VERSION STAMP. Bumped from C8.0 → C8.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C8.1', AND the JSON export __version__ field now reads 'C8.1'. The CSV download _<VERSION> suffix automatically advances to "_C8.1". All prior changelog rows (C8.0 through B2.8) are preserved.
C8.0May 2026Marketing bump for the first prod-ready release after the Cloudflare Pages deploy. Consolidates the C7.8 staging-gate fix (login now checks the cloud profile FIRST for the canUseStaging permission, falling back to local only when the cloud is offline) and the C7.9 password-mirror + title-preservation fixes (self-password changes now push to Supabase Auth alongside local; Connect to Supabase preserves the local title on the profile row so Test Accounts stay Test Accounts). No new features on top of C7.9 — this is purely a version-number bump for the first stable release. VERSION STAMP. Bumped from C7.9 → C8.0 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C8.0', AND the JSON export __version__ field now reads 'C8.0'. The CSV download _<VERSION> suffix automatically advances to "_C8.0". All prior changelog rows (C7.9 through B2.8) are preserved.
C7.9May 2026Two password/title stability fixes. (1) SELF-PASSWORD CHANGE NOW PUSHES TO SUPABASE AUTH. When you change your own password via the Users tab, the code previously only wrote the new value to local. Supabase Auth kept the OLD password. On next login with the old password, the C6.6 password auto-sync would overwrite your local password back to the old value, silently reverting your change. C7.9 adds a self-change mirror: supabase.auth.updateUser({ password: newPass }) fires alongside the local write when isOverride is false and the target row matches the currently-signed-in Supabase user by email. Now local AND cloud both hold the new password from the moment you click Save, and C6.6's auto-sync just re-caches the same value harmlessly. (2) CONNECT TO SUPABASE PRESERVES LOCAL TITLE. The C7.2 _cloudProvisionAccount was hardcoding every new profile row with title: 'Operations'. When a Test Account got connected, its profile row said "Operations" even though the local record said "Test Account". On next cloud login the C5.5 profile mirror pulled the Operations title back down, clobbering the local Test Account title. That broke the C7.1 Test-Account-only-local login fast path because the account was no longer treated as a Test Account. C7.9 now passes the local user's current title into _cloudProvisionAccount, so a Test Account stays a Test Account across every login cycle. (3) VERSION STAMP. Bumped from C7.8 → C7.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.9', AND the JSON export __version__ field now reads 'C7.9'. The CSV download _<VERSION> suffix automatically advances to "_C7.9". All prior changelog rows (C7.8 through B2.8) are preserved.
C7.8May 2026Staging gate now consults the cloud profile before local. (1) The C6.7 gate ran at the very top of the login handler and rejected any non-Owner without a local canUseStaging flag. Fresh browsers (e.g. Chrome after Safari toggled the pill) had a stale local flag because no cloud login had run yet, so they were blocked even though Supabase said the user had permission. (2) C7.8 rewrites the gate: it now looks up the current user's profile row on Supabase by email BEFORE the local check. If the cloud row exists and can_use_staging is TRUE, the login is allowed and the local record is also refreshed with canUseStaging = true so subsequent gate checks stay cheap. If the cloud is unreachable (offline), the flow falls back to the local canUseStaging flag exactly like C6.7 did — so offline emergency Owner login still works. (3) VERSION STAMP. Bumped from C7.7 → C7.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.8', AND the JSON export __version__ field now reads 'C7.8'. The CSV download _<VERSION> suffix automatically advances to "_C7.8". All prior changelog rows (C7.7 through B2.8) are preserved.
C7.7May 2026User list now syncs across browsers via public.profiles. (1) LOGIN PULLS EVERY PROFILE. On every successful cloud sign-in, right after the caller’s own profile row is mirrored, the app now fetches every row from public.profiles and reconciles them into the local users store: any profile whose handle isn’t yet in the local list gets added as a fresh local user record with title, email, and every mirrored permission column copied down. Existing local users are left alone (permissions were already mirrored per-user by C7.5). Local users that don’t exist in cloud are NOT deleted — that’s an opt-in safety measure so a temporary Supabase outage can’t wipe your Sand Box card locally. (2) REALTIME INSERT/DELETE FOR PROFILES. The existing profiles Realtime subscription (from C7.5) now also handles INSERT payloads: a new profile row created on browser A appears in the Users tab of browser B within ~1 second. DELETE payloads scrub the corresponding local user record (respecting the safety rule above: cloud deletes DO reflect locally because the DELETE was explicitly authored on the other browser, not just a missing row). (3) TWO-BROWSER TESTING NOW WORKS END TO END. Create an account in Safari via Add Account or Connect to Supabase → Chrome sees the new account within a second (once you’re signed into cloud on Chrome). Change a permission on Safari → Chrome mirrors it. Delete the account on either browser → both browsers lose it. (4) SCHEMA PATCH NOT REQUIRED. C7.7 reuses the columns C7.5 already added to profiles. If you’ve run GB-BizCalc - Supabase Permissions Extension.sql on both projects, C7.7’s sync is live immediately with no additional SQL. (5) VERSION STAMP. Bumped from C7.6 → C7.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.7', AND the JSON export __version__ field now reads 'C7.7'. The CSV download _<VERSION> suffix automatically advances to "_C7.7". All prior changelog rows (C7.6 through B2.8) are preserved.
C7.6May 2026Tiny quality-of-life fix on top of C7.5. (1) TITLE CHANGE INSTANTLY REFRESHES THE PILLS ROW. Before C7.6, changing a user's Title dropdown updated their u.title value but did NOT re-render the Users tab, so pills that depend on the title (Connect to Supabase, Use Owner Password — both Test-Account-only) didn't appear or disappear until you closed Settings and reopened it. C7.6 makes gbUpdateUserField call gbSettingsSwitchTab('users') whenever the changed field is title, cloudConnected, isOwner, or useOwnerLogin. Every other field (name, email, altEmail, otherLabel, permission pills) skips the re-render so typing in the name/email input doesn't lose focus. (2) VERSION STAMP. Bumped from C7.5 → C7.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.6', AND the JSON export __version__ field now reads 'C7.6'. The CSV download _<VERSION> suffix automatically advances to "_C7.6". All prior changelog rows (C7.5 through B2.8) are preserved.
C7.5May 2026Cross-browser permission sync ships. (1) FULL PERMISSION MIRROR VIA public.profiles. The profiles table now carries every Owner-editable permission column (can_see_full_guide, can_delete_accounts, can_see_graveyard, can_manage_role_perms, can_manage_titles, can_see_changelog, can_add_accounts, can_see_cloud_tab, can_see_backup_tab, can_use_staging, cloud_connected). Schema patch delivered alongside this build as GB-BizCalc - Supabase Permissions Extension.sql — run it on both projects before this build's cross-browser sync will work. (2) LOGIN MIRRORS ALL COLUMNS DOWN. C5.5's _mergeProfileFromCloud now reads every permission column from the profiles row and applies it to the local user record on cloud sign-in. If Chrome and Safari both cloud-log-in Sand Box, they end up with identical permission state because both pulled it from the same profiles row. (3) PILL TOGGLES PUSH TO PROFILES. Whenever the Owner flips a permission pill on any user card, the change is now mirrored to that user's profiles row via Supabase (fire-and-forget, non-blocking). Local write still happens instantly for the current browser; the cloud write happens in the background. (4) REALTIME PROFILES SUBSCRIPTION. Every browser now subscribes to postgres_changes on public.profiles alongside the existing app_state subscription. When one browser toggles a permission, every other browser signed into the same project receives the update within a second and merges it into its local users store. Cross-browser permission drift is now impossible. (5) SCHEMA MIGRATION IS OPT-IN. If you don't run the SQL patch yet, C7.5 still works — the mirror reads whichever columns exist and skips the rest gracefully. Adding the columns unlocks the full sync; not adding them just falls back to C7.4 behavior for those specific permissions. (6) VERSION STAMP. Bumped from C7.4 → C7.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.5', AND the JSON export __version__ field now reads 'C7.5'. The CSV download _<VERSION> suffix automatically advances to "_C7.5". All prior changelog rows (C7.4 through B2.8) are preserved.
C7.4May 2026Hot-fix on top of C7.3. C7.3 shipped the MFR/LANDED Cost Ref. Chart HISTORY modal + JS by injecting them right before the final </body> tag. The injection ran html.replace('</body>' ...) which matched the FIRST </body> in the file — and there was one inside _gbCalcPopupHtml()'s JavaScript template literal (the GB-Calc popup window markup ends in </body></html>). So the modal + JS landed inside a JS string that lives inside another <script> tag. When the HTML parser hit the injected</script> tag, it terminated the OUTER script block right there (HTML parser rule: </script> anywhere in a script closes it, even inside a JS string), leaving the rest of the file to render as raw text on the homepage. That broke every downstream handler (tabs, buttons, everything). C7.4 cuts the misplaced block out of the JS string, re-inserts it before the REAL </body> at end of file (found via rfind so the search runs right-to-left and can't collide with any embedded </body>), and bumps the version. All C7.3 features (Realtime sync, lock initials, chart history) are preserved. Version stamp: bumped from C7.3 → C7.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.4', AND the JSON export __version__ field now reads 'C7.4'. The CSV download _<VERSION> suffix automatically advances to "_C7.4". All prior changelog rows (C7.3 through B2.8) are preserved.
C7.3May 2026MFR + LANDED history ships. (1) HISTORY BUTTON. A new dark-yellow HISTORY pill sits between the NOTES button and the orange Download PDF button in the MFR / LANDED chart header row. Click it to open the History modal for whichever chart is currently active (MFR when the MFR heading is on top; LANDED when the LANDED heading is on top). (2) HISTORY MODAL. Lists every saved snapshot for the active chart. Top row has a "+ Save current as snapshot" button that prompts you for a name, then freezes the current chart values (cells, prior values, timestamps, custom columns) as a snapshot in localStorage. Each row shows: favorite star, name, timestamp, and four actions — Load (restores that snapshot into the live chart), PDF (downloads the snapshot as a PDF via the existing chart PDF exporter), Delete (removes with confirm), and a favorite toggle. (3) SEPARATE HISTORIES PER CHART. MFR snapshots live under gbBizCalcMfrCostRefHistory_v1; LANDED snapshots live under gbBizCalcLandedCostRefHistory_v1. They never mix. (4) FAVORITES SORT FIRST. Snapshots marked favorite sort to the top of the list, then non-favorites by save-date descending. (5) LOAD IS UNDOABLE VIA A CURRENT SNAPSHOT. Before Load overwrites the live chart, the flow first saves the current live state as an auto-snapshot named "Before load: [snapshot name]" so you can restore if the load wasn't what you wanted. (6) SNAPSHOTS SYNC. The two new storage keys are added to the C6.8 Realtime sync channel, so a snapshot you save on your laptop appears in Jason's browser automatically. (7) VERSION STAMP. Bumped from C7.2 → C7.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.3', AND the JSON export __version__ field now reads 'C7.3'. The CSV download _<VERSION> suffix automatically advances to "_C7.3". All prior changelog rows (C7.2 through B2.8) are preserved.
C7.2May 2026One new opt-in feature on top of C7.1. (1) TEST ACCOUNT "CONNECT TO SUPABASE" PILL. Owner-only visibility on Test Account cards. Default OFF. When you flip it ON: the app prompts for an email + a strong password, then runs the same cloud provision flow used for real accounts (Add Account → signUp on temp non-persisting client + insert profiles row via Owner client). The Test Account now has a real auth.users row and public.profiles row, but is_owner remains false. When you flip it OFF: the app deletes the profiles row (kills their cloud login) and reminds you to also delete the auth.users row from the Supabase dashboard if you want a full cleanup. (2) LOGIN FLOW BRANCHES ON cloudConnected. If a Test Account has cloudConnected OFF, the C7.1 fast path still fires (local-only login). If cloudConnected is ON, the Test Account routes through the normal cloud-first flow, exercising signInWithPassword, profile mirror, and Realtime just like any real user. Perfect for validating end-to-end cloud behavior without risking a real user account. (3) STORED IN cloudConnected FIELD. Seeded + backfilled on all users. Also mirrored from a new optional profiles column (if present); harmless if you leave the schema alone. (4) RATE-LIMIT WARNING. The confirm dialog notes that each Connect-to-Supabase toggle burns one signup attempt against Supabase's default 4-emails-per-hour cap. Don't chain these back-to-back on multiple Test Accounts. (5) VERSION STAMP. Bumped from C7.1 → C7.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.2', AND the JSON export __version__ field now reads 'C7.2'. The CSV download _<VERSION> suffix automatically advances to "_C7.2". All prior changelog rows (C7.1 through B2.8) are preserved.
C7.1May 2026One targeted fix on top of C7.0. (1) TEST ACCOUNT LOGIN BYPASSES SUPABASE ENTIRELY. Test Accounts have no Supabase auth user by design (they are local-only scratch accounts for permission-gate testing). C6.5-C7.0 accidentally routed Test Account sign-in through Supabase first, so any Test Account with "Use Owner Password" turned OFF had no login path at all — the cloud check failed (no cloud user), then the offline fallback only accepted Owner or Test-Account-with-useOwnerLogin-ON, so the test account's own password was silently rejected. C7.1 adds a fast path at the top of the login flow: if u.title === 'Test Account', skip the Supabase attempt and go straight to the local password check, which accepts (a) the account's own password OR (b) when useOwnerLogin is on, the Owner's password. Fixes the exact scenario the Owner just ran into with Sand Box. (2) VERSION STAMP. Bumped from C7.0 → C7.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.1', AND the JSON export __version__ field now reads 'C7.1'. The CSV download _<VERSION> suffix automatically advances to "_C7.1". All prior changelog rows (C7.0 through B2.8) are preserved.
C7.0May 2026Admin RPC ships. (1) OWNER PASSWORD OVERRIDE NOW HITS SUPABASE AUTH. When Owner changes another user's password via the Users tab (after the C6.1 "Override user X's password?" confirm), the app now also invokes a Supabase Edge Function called admin-set-password to update that user's Supabase Auth password. Local cache still updates for the offline fallback. Client passes: the target user's Supabase UUID (cached in the local record as supaUid, or looked up by handle if missing) and the new password. The Edge Function verifies the caller's JWT belongs to an Owner in public.profiles and calls auth.admin.updateUserById(uid, { password }) via the service_role key held in Supabase env vars. Result modal notes success or the exact error. (2) EDGE FUNCTION SOURCE + DEPLOY GUIDE. Delivered alongside this build as admin-set-password/index.ts (Deno / TypeScript). Deploy with the Supabase CLI: supabase functions deploy admin-set-password --project-ref <project_ref>. Then set the function's SERVICE_ROLE_KEY secret via the dashboard or supabase secrets set SERVICE_ROLE_KEY=<key>. Deploy on BOTH staging AND prod. (3) OFFLINE / MISSING FUNCTION FALLBACK. If the Edge Function isn't deployed yet or the browser is offline, the local password change still succeeds and the alert tells you the cloud override was skipped. Deferred to C7.1: MFR + LANDED Cost Ref. Chart History button (reload/PDF/delete/favorites), modeling on the SHIPPING REFERENCE HISTORY flow. (4) VERSION STAMP. Bumped from C6.9 → C7.0 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C7.0', AND the JSON export __version__ field now reads 'C7.0'. The CSV download _<VERSION> suffix automatically advances to "_C7.0". All prior changelog rows (C6.9 through B2.8) are preserved.
C6.9May 2026Two visible polish fixes on top of C6.8. (1) SYNC PILL HAS A VISIBLE IDLE STATE. Previously the SYNC status pill was invisible when idle so you had no idea where in the header row it was. C6.9 gives it a permanent faint gray dot "\u25CB SYNC" when idle so you can always see the indicator's spot; it flips to blue SYNCING, green SYNCED, or red SYNC ERROR as before. Also nudged +2px of margin so it stops kissing the initials pill. (2) "GB-BizCalc" HEADING IS NOW A LINK. Click the words "GB-BizCalc" in the top-left tagline ("GB-BizCalc - Internal Vibe Coded Biz Software") to jump to the live URL for whichever environment the app auto-routed to: https://gb-bizcalc-staging.dev when the current env is staging, https://gb-bizcalc.dev when it is prod. If you have not yet deployed to Cloudflare Pages, clicking the link will show Cloudflare's "domain unclaimed" landing page for now — that's the expected placeholder until you push the file to a Pages project. DEFERRED FOR C7.0: (a) MFR + LANDED Cost Ref. Chart History button with reload / PDF / delete / favorites (will model on the SHIPPING REFERENCE chart's SEE HISTORY flow), and (b) Admin RPC so Owner-changing-a-user's-password also pushes to Supabase Auth via a Supabase Edge Function (requires deploying a small server-side function). Both queued next. (3) VERSION STAMP. Bumped from C6.8 → C6.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.9', AND the JSON export __version__ field now reads 'C6.9'. The CSV download _<VERSION> suffix automatically advances to "_C6.9". All prior changelog rows (C6.8 through B2.8) are preserved.
C6.8May 2026Two big features on top of C6.7. (1) REALTIME CUSTOMER CARD SYNC. Any change to Customer Cards data (gbBizCalcCustomerInfo_v1) or the ROI Cards store (gbBizCalcRoiCards_v1) or the Sub-Basis Lock table (gbBizCalcRoiSubBasisLock_v1) is now pushed to the Supabase public.app_state table via a debounced upsert (400ms tail so a burst of typing sends one payload, not twenty). On every browser signed into the same project, a Realtime subscription on app_state hydrates that key back into localStorage the instant the remote row changes, then triggers a UI re-render. Loop protection: writes we just applied from a remote payload don't re-push. Self-writes we did don't re-apply. (2) SYNC STATUS BADGE. In the header row next to the env floater, a small pill now reads SYNCING (blue) while a push is in flight, SYNCED (green) when acknowledged, or SYNC ERROR (red) if Supabase rejects. On page load and while offline it stays blank. (3) PROFILE-LOCK OWNER INITIALS. When Owner (or any permitted user) locks a profile to a cost basis, the active user's initials (MG, TA, etc.) are now stored alongside the lock in a parallel gbBizCalcRoiSubBasisLockOwner_v1 map. The padlock badge on the locked cost basis chip renders the initials in small black letters immediately to the right of the padlock icon, before the profile letter. So a padlock next to "MG P1" means Mark locked Profile 1. Same UPC-under-10-pack style: small, mono, black. The initials sync via app_state too, so all browsers see who did what within a second. (4) ROI-CARDS AND SUB-BASIS LOCKS ARE ALSO SYNCED. Not just CINFO. All three storage keys ship through the same app_state channel so full workspace mirroring works. (5) VERSION STAMP. Bumped from C6.7 → C6.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.8', AND the JSON export __version__ field now reads 'C6.8'. The CSV download _<VERSION> suffix automatically advances to "_C6.8". All prior changelog rows (C6.7 through B2.8) are preserved.
C6.7May 2026One feature on top of C6.6. (1) NEW "STAGING" PERMISSION PILL. Owner-only visibility on every non-Owner card. Default OFF for all users and all title defaults. When SUPA_ENV === 'staging' the login flow rejects any non-Owner account that does NOT have canUseStaging ON with a clear message: "This account does not have staging access. Ask the Owner to grant Staging permission." Owner is always allowed on staging (and everywhere else). This means real user accounts (Jason and friends) will NEVER accidentally sign into staging even if they somehow land on the staging domain, unless you explicitly grant them Staging for testing. Test accounts you spin up specifically for staging can have this on; production users leave it off. (2) ROLE PERMISSIONS DEFAULT MAP UPDATED. The Role Permissions section shows canUseStaging alongside the other pills, so you can set it as a default for any role. Test Account defaults to off (you can turn it on per-account after creation, or set it as a role default if all your Test Accounts are for staging). (3) SWITCHING ENVS SIGNS EVERYONE OUT (already true). When you use Force STAGING or Force PROD in the Cloud tab, the app already signs the current session out and reloads. So on next login the staging gate will kick in immediately if the signed-in account lacks staging permission. (4) VERSION STAMP. Bumped from C6.6 → C6.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.7', AND the JSON export __version__ field now reads 'C6.7'. The CSV download _<VERSION> suffix automatically advances to "_C6.7". All prior changelog rows (C6.6 through B2.8) are preserved.
C6.6May 2026Four fixes on top of C6.5. (1) "USE OWNER PASSWORD" PILL IS NOW OWNER-ONLY. Test accounts logged into their own account no longer see or can toggle the Use Owner Password pill. Only the Owner sees it (on Test Account cards) and can flip it. Non-Owner Test-account viewers looking at their own card see the other permission pills the Owner granted them, but not this one — because that's an Owner-managed security setting, not a self-preference. (2) DELETE ALSO REMOVES THE SUPABASE PROFILES ROW. When Owner clicks the red Delete button on a user card, the app now: (a) moves the local record to the graveyard as before, (b) looks up that user's Supabase UUID by handle if not already cached, and (c) deletes the public.profiles row via the Owner-authenticated client. That kills their ability to log in immediately — the profile mirror on login won't find their row so their session can't hydrate. The auth.users row still exists in Supabase (only service_role can delete those from the browser); the graveyard modal notes this and reminds you to clean that up in the dashboard if you want. (3) REVIVE RE-INSERTS THE PROFILE ROW. When you click Revive from the graveyard, the app puts the local record back AND re-inserts the profiles row into Supabase using the stored UUID (auth.users row was never deleted, so this just restores the profile side). The revived user can log in again with the same password immediately. (4) NEW supaUid FIELD. Every account created via Add Account from C6.6 forward stores the Supabase UUID it got at signup, so later delete/revive/admin actions don't need a lookup. Legacy accounts (like Jason if he already exists) get their UUID resolved on first delete attempt and cached. (5) RATE-LIMIT ERROR NOW ACTIONABLE. If Supabase says "email rate limit exceeded" on signUp, the result modal now explains why (default Supabase SMTP is capped at ~4 emails/hour) and gives you the two options: wait ~30-60 min, OR configure a custom SMTP provider (Resend, SendGrid, Mailgun) at Auth → SMTP Settings. (6) VERSION STAMP. Bumped from C6.5 → C6.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.6', AND the JSON export __version__ field now reads 'C6.6'. The CSV download _<VERSION> suffix automatically advances to "_C6.6". All prior changelog rows (C6.5 through B2.8) are preserved.
C6.5May 2026Three targeted fixes on top of C6.4. (1) EMAIL-TEMP-PASSWORD CHECKBOX REBUILT AS A PILL. Native <input type="checkbox"> refused to accept clicks inside the Add Account modal across two iterations. C6.5 replaces the checkbox entirely with a button-pill toggle that uses the exact same data-on pattern used successfully everywhere else in the app (Test Account "Use Owner Password", "Alt email OK", all the permission pills on user cards). Click the pill to flip its dot from empty to filled. On submit the flow reads data-on directly. (2) DEFAULT IS NOW OFF. If you don't click the pill, Add Account creates the account WITHOUT emailing anyone. You share the temp password however you want. If you click the pill on before Make Account, the mailto pops up as before. (3) PLACEHOLDER TEXT REPLACED. The ghost text in the Name field now reads "Billy Madison" and the ghost text in the Email field reads "ChrisFarley@example.com". Just examples, no relation to any real account. (4) VERSION STAMP. Bumped from C6.4 → C6.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.5', AND the JSON export __version__ field now reads 'C6.5'. The CSV download _<VERSION> suffix automatically advances to "_C6.5". All prior changelog rows (C6.4 through B2.8) are preserved.
C6.4May 2026Two fixes on top of C6.3 to unblock Add Account for real. (1) EMAIL-TEMP-PASSWORD CHECKBOX FIXED (AGAIN). The C6.1/C6.2 refactor of the checkbox row into a wrapping label still refused to accept clicks in practice. C6.4 rebuilds the row as a plain container: <div> holding a bare <input type="checkbox"> and a <label for="gbAddEmailPass">, both with explicit pointer-events:auto and cursor:pointer. Click either the box or the words — both toggle it. Also styled as a faint gray card so it visually reads as a control. (2) HELPFUL RLS ERROR IN THE RESULT MODAL. When the Supabase profile insert is rejected by a row-level security policy (the "new row violates row-level security policy" error), the alert now tells you EXACTLY what to do: run the SQL patch GB-BizCalc - Supabase RLS Patch.sql that ships with this build in the SQL Editor on BOTH staging and prod. That patch rebuilds public.is_owner() with SECURITY DEFINER + fixed search_path so the Owner-only insert policy can reliably read the Owner flag from the profiles table even when the RLS check itself is what triggered the read. Root cause: the function ran without elevated privileges, so its own SELECT was sometimes blocked by RLS visibility during policy evaluation and returned false. (3) VERSION STAMP. Bumped from C6.3 → C6.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.4', AND the JSON export __version__ field now reads 'C6.4'. The CSV download _<VERSION> suffix automatically advances to "_C6.4". All prior changelog rows (C6.3 through B2.8) are preserved.
C6.3May 2026Visibility overhaul on top of C6.2. (1) CHANGELOG HIDDEN FROM NON-OWNERS. The Changelog block in the User Guide (the one with the search widget + BIG button) now only renders for the Owner or accounts with the new "Can see changelog" pill on. (2) CLOUD + BACKUP TABS HIDDEN FROM NON-OWNERS. The two Settings tabs are hidden entirely from non-Owner viewers unless you turn on the new "Can see cloud tab" / "Can see backup tab" pills. (3) "+ ADD ACCOUNT" HIDDEN UNLESS PERMITTED. The button is Owner-only by default. Grant "Can add accounts" to any other account to restore it. (4) USERS TAB RENAMED "USER" FOR RESTRICTED VIEWERS. Non-Owner viewers without Can-add-accounts see the settings tab labeled USER and their card is the only one in it. Non-Owner viewers WITH Can-add-accounts see it labeled USERS and can see every account except the Owner. Owner always sees USERS. (5) AUDIT LOG STALKER SENTENCE REMOVED. Deleted "Non-owner accounts see only their own history so nobody feels stalked". That phrasing was, uh, a lot. (6) FOUR NEW PILLS. All Owner-only visibility on non-Owner cards, all default off: Can see changelog, Can add accounts, Can see cloud tab, Can see backup tab. Turn any on to restore that access to a specific user. (7) TITLE PERMISSIONS DEFAULTS UPDATED. The new four permissions are wired into the Role Permissions section too, so you can set them at the title/role level as defaults for future accounts. (8) VERSION STAMP. Bumped from C6.2 → C6.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.3', AND the JSON export __version__ field now reads 'C6.3'. The CSV download _<VERSION> suffix automatically advances to "_C6.3". All prior changelog rows (C6.2 through B2.8) are preserved.
C6.2May 2026Hot-fix on top of C6.1. C6.1 accidentally broke the header account-name pill (the ppl icon / MG Owner button) — clicking it did nothing, which meant Settings and Sign Out were unreachable. Root cause: the C6.1 script that added a "confirm before removing a custom title" dialog wrapped the body of gbRemoveTitle in an IIFE but appended the closing })(); at the wrong anchor — it landed inside gbBackupFullClick. That single misplaced token was a JavaScript parse error, which kills every handler declared later in the script block (including gbUserToggleMenu). C6.2 removes the stray })(); from gbBackupFullClick and rebuilds gbRemoveTitle as a plain function with a single confirm at the top — no IIFE at all. Every C6.1 feature (Owner invisible to non-Owners, read-only other cards, password eye toggle, Owner override confirm, Titles gate, canManageTitles pill, custom title delete confirm, profile picture upload, fixed Email checkbox) is preserved — only the syntax fault is patched. Also version bumped from C6.1 → C6.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.2', AND the JSON export __version__ field now reads 'C6.2'. The CSV download _<VERSION> suffix automatically advances to "_C6.2". All prior changelog rows (C6.1 through B2.8) are preserved.
C6.1May 2026Nine fixes/features on top of C6.0 plus the version bump. Everything here is safe on top of C6.0. (1) OWNER ACCOUNT INVISIBLE TO NON-OWNERS. In Settings → Users, non-Owner viewers now see only accounts other than the Owner. The Owner card is fully hidden from their card list — you can grant this later if you want to via a future permission. (2) NON-OWNER READ-ONLY ON OTHERS. When a non-Owner is looking at any account that is not their own, every field (name, email, title, permission pills, delete button, change-password button, password eye toggle) is disabled or hidden. Their OWN card stays fully editable. (3) OWNER PASSWORD OVERRIDE CONFIRM. When Owner changes someone else's password, a confirm dialog now pops: "Override user NAME's password?". Yes saves; No cancels. Prevents accidental clobber. Owner-changed passwords still only update the local cache; the user's Supabase password stays intact until they change it via Users tab themselves. (4) PASSWORD EYE TOGGLE. All password inputs (login overlay, change-password form) now include a faded eyeball icon inside the right edge of the input. Click to reveal typed characters as plain text; click again to hide. On other accounts' password fields the eyeball is inert (matches the read-only rule above). (5) EMAIL-TEMP-PASSWORD CHECKBOX FIXED. The checkbox in the Add Account modal is now properly clickable; the label wraps it so clicking either the box or the words toggles it. (6) TITLES SECTION HIDDEN FROM NON-OWNERS. The "Titles" box at the top of the Users tab (with the "+ Add title" input) now only shows to Owner or accounts with the new "Can manage titles" pill. Non-permitted users just see Accounts. (7) NEW "CAN MANAGE TITLES" PERMISSION. Owner-only visibility on non-Owner cards. Default off. Grants access to the Titles section. (8) CUSTOM TITLE DELETE CONFIRM. Removing a custom title now shows a confirm popup: "Delete title X? Any accounts still using it stay as-is.". Default titles remain permanent + hide their remove button. (9) PROFILE PICTURE UPLOAD. The colored initials circle is now a click target for uploading a picture. Left-click the circle from the viewer's own settings (Owner can click anyone's), opens the OS file picker for images, then the app auto-crops the image to a centered square and stores it as a 256x256 JPEG data URL inside the user's record. The card avatar renders that image inside the same circular space, letting the natural center-out crop show through. Any user can upload their own; only Owner can upload for someone else. (10) VERSION STAMP. Bumped from C6.0 → C6.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C6.1', AND the JSON export __version__ field now reads 'C6.1'. The CSV download _<VERSION> suffix automatically advances to "_C6.1". All prior changelog rows (C6.0 through B2.8) are preserved.
C6.0May 2026Big batch of Users-tab and account-lifecycle features on top of C5.9. (1) SAND BOX LOGIN FIXED. The C5.5 login flow rejected a Test Account trying to log in with the Owner's password after Supabase said "wrong password" (because Sand Box has no Supabase user by design). C5.10 restores the C5.0 behaviour: when Supabase says wrong password AND the account is a Test Account with "Use Owner Password" on AND the entered password matches the local Owner password, the login succeeds via the offline local check. Everyone else still gets rejected immediately. (2) "USE OWNER LOGIN" RENAMED TO "USE OWNER PASSWORD". The pill text on Test Accounts and the wording in the login overlay hint now say "Use Owner Password" because that's all it does — accept the Owner's password as a valid password for the Test Account too. (3) "+ ADD ACCOUNT" CAPITAL A. Button label corrected. (4) DELETE ACCOUNT BUTTON. Each non-Owner user card now has a small red "Delete" button in its header. Clicking it opens a confirm dialog: "Are you sure you want to delete NAME's account?". Confirming moves the account to a local "graveyard" store (soft delete). Owner-only visibility by default; a new "Can delete accounts" pill grants any account the ability to delete other accounts. (5) ACC. GRAVEYARD BUTTON. Sits directly to the right of "+ Add Account" on the Users tab. Owner-only visibility by default; a new "Can see graveyard" pill grants any account access to the graveyard. Clicking it opens a modal listing every soft-deleted account with their name / email / date deleted. Each row has a green "Revive" button (moves the account back to the active list at the exact state it was in before delete) and a red "Permanently Delete" button (removes the account from the graveyard entirely; also reminds you to delete the auth.users row in Supabase manually since Owner cannot delete auth users from the browser). (6) NEW ADD-ACCOUNT MODAL. The old three-native-prompt flow is replaced with a proper inline modal: Name, Email, Role (dropdown of Titles), a "Email temp password to user" checkbox, and a big "Make Account" button. When Make Account is clicked the app: (a) validates fields, (b) generates a strong random temp password, (c) provisions the user in Supabase Auth via the non-persisting temp-client pattern (C5.9), (d) inserts the profiles row via Owner's client, (e) mirrors the account into local, and (f) if "Email temp password" is on, opens the user's default email client via a mailto: link pre-composed with a welcome message, the role, the temp password, and change-password instructions. Owner just clicks Send in their email client. (7) VERSION STAMP. Bumped from C5.9 → C5.10 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.10', AND the JSON export __version__ field now reads 'C5.10'. The CSV download _<VERSION> suffix automatically advances to "_C5.10". All prior changelog rows (C5.9 through B2.8) are preserved.
Extra additions on top of the original C5.10 note above (a mistaken double-digit version label — wiped throughout the app and superseded by this C6.0): (a) HIDDEN GUIDE HEADING RECOLORED. In the User Guide the "Hidden Guide" tab next to "Table of Contents" is now RED (was blue). When active, saturated red + red underline. When inactive, faded light red (same brightness level as the prior gray). Also visually distinct so you never mistake it for the normal TOC tab. (b) NEW "OTHER" TITLE. Added to the default Titles list. When a user selects "Other" as their title, a small custom-label input appears beside the title dropdown so they (or the Owner editing their card) can type a private sub-label. In the Users list and every other summary place, the title still displays as "Other" — the sub-label is only visible on the user's own card. (c) TITLE-LEVEL DEFAULT PERMISSIONS. Added a new titlePermissions store keyed by title name. When the Owner (or a user with the new "Can manage role permissions" pill) creates a new account with title X, that account's permission pills default to titlePermissions[X]. Test Account title defaults match the old Sand Box exactly (useOwnerLogin ON, everything else off). "Other" title defaults are all OFF. Non-Test titles default to Jason's current permission shape (all off). (d) NEW ROLE PERMISSIONS SECTION. In Settings → Users a new Role Permissions box appears above the Accounts list for Owner (or anyone with the new pill). Pick a title from a dropdown, see every permission field with an on/off toggle showing the default for THAT title. Toggling saves to titlePermissions. Existing accounts are untouched; only future Add-Account creations pick up the new defaults. (e) NEW "CAN MANAGE ROLE PERMISSIONS" PILL. Owner-only visibility on non-Owner cards. Default off. Grants the ability to see + edit the Role Permissions section. (f) C5.10 LABEL WIPED. Every "C5.10" reference in code, comments, filenames, PDF footers, JSON export version field, and CSV suffix is rewritten to "C6.0". Only this note documents that C5.10 ever briefly existed.
C5.9May 2026Three fixes plus one big feature on top of C5.8. (1) TOC TAB COLORS FIXED. The active tab in "Table of Contents | Hidden Guide" is now blue + underlined; the inactive tab is gray. Previously the colors were reversed — you had to click the underlined blue one to see "which was the current view", which was backwards. (2) H1 SECTION RENAMED. The first hidden guide section was "What Changed Tonight — Cloud + Local Overview". Trimmed to just "Cloud + Local Overview" in both the TOC link and the h2 heading. (3) ADD ACCOUNT NOW PROVISIONS THROUGH SUPABASE. When the Owner clicks "+ Add account" on the Users tab, the flow now: (a) prompts for name + email as before, (b) generates a strong random temp password, (c) creates the account in Supabase Auth via a temporary non-persisting client (so the signUp does NOT sign the Owner out of their own session), (d) inserts the matching row into public.profiles using the Owner's authenticated client, (e) mirrors the account into the local users store, (f) shows a modal with the email and temp password for you to share with the new user out of band. (4) REQUIRES "CONFIRM EMAIL" OFF ON BOTH SUPABASE PROJECTS. Because we are provisioning accounts that don't need to click a confirmation email, both projects must have Authentication → Providers → Email → "Confirm email" toggled OFF. If the toggle is ON, the new user will still be created but they cannot log in until they click a confirmation link that Supabase tries to email — and their email may not be deliverable yet, so their account will be stuck. Turn Confirm email OFF on staging AND prod before you use Add Account for real. (5) OFFLINE FALLBACK. If Supabase is unreachable when you click Add Account, the flow falls back to the C5.0 local-only path (creates the account in localStorage only). A yellow warning banner in the result modal reminds you the account still needs to be provisioned in Supabase later once you are back online. (6) VERSION STAMP. Bumped from C5.8 → C5.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.9', AND the JSON export __version__ field now reads 'C5.9'. The CSV download _<VERSION> suffix automatically advances to "_C5.9". All prior changelog rows (C5.8 through B2.8) are preserved.
C5.8May 2026Five UI polish fixes on top of C5.7 plus the version bump. Nothing about how login/auth/sync works changes. (1) SETTINGS MOVED INTO THE USER DROPDOWN. The dedicated ⚙ gear icon in the header row is removed. The account menu (opened by clicking your name/initials pill) now includes a "Settings" row that opens the same modal. Cleaner header, one fewer control to explain. (2) OFFLINE DETECTION NOW REACTS TO NETWORK DROPS. The header env floater used to only flip to OFFLINE when the Supabase library never loaded at all; if the library loaded and then internet dropped later, the floater kept saying STAGING MODE. C5.8 also checks navigator.onLine and listens to the browser's online / offline events, so the floater flips to OFFLINE in red the instant your connection drops, and back to STAGING MODE (or blank on prod) the instant it comes back. (3) PILL RENAMED. The Owner-only permission pill on non-Owner user cards is renamed from "Hidden User Guide Access" to "Hidden User Guide Access" for clarity. (4) HIDDEN GUIDE MOVES TO A TAB NEXT TO THE TOC HEADING. The big orange collapsible "▶ HIDDEN USER GUIDE" button under the User Guide TOC is removed. In its place, when the current viewer is Owner or has Hidden User Guide Access on, the TOC heading now reads "Table of Contents | Hidden Guide" with both parts clickable. Table of Contents is active by default; clicking Hidden Guide swaps the 19-entry visible list for the 8-entry hidden list, and clicking Table of Contents swaps back. Non-permitted users see no Hidden Guide label at all. (5) LONG CODE STRINGS IN THE HIDDEN GUIDE NOW WRAP. The JWT anon keys and any long URLs / IDs shown inside the hidden guide sections used to overflow the User Guide popup on narrow screens. C5.8 forces word-break: break-all + overflow-wrap: anywhere on every <code> block inside the hidden guide so long strings wrap onto multiple lines inside the popup instead of running off to the right. (6) VERSION STAMP. Bumped from C5.7 → C5.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.8', AND the JSON export __version__ field now reads 'C5.8'. The CSV download _<VERSION> suffix automatically advances to "_C5.8". All prior changelog rows (C5.7 through B2.8) are preserved.
C5.7May 2026Three additions on top of C5.6 plus the version bump. All three are UI-only and change nothing about how the C5.5/C5.6 Supabase auth flow works. (1) ENV INDICATOR FLOATER. Left of the gear icon in the header row a small floating text label now shows the current environment when it is NOT normal production: "STAGING MODE" in green when the routed env is staging, or "OFFLINE" in red when the Supabase client failed to load. When you are on prod and the client is up, the label is empty so nothing shows — that's the "working normally" state. The floater re-checks itself when the Cloud tab overrides env and whenever the login overlay opens. (2) "CAN SEE FULL USER GUIDE" PERMISSION. Every non-Owner user card now has a fifth pill toggle: "Hidden User Guide Access". Owner-only visibility in the Users tab (grants a hidden guide flag saved to that account's canSeeFullGuide field). Default off. When Owner is signed in they can always see the full guide; other users only see it when this pill is on. (3) HIDDEN USER GUIDE SECTION. The User Guide now includes a second heading, "Hidden User Guide", that sits to the right of "Table of Contents" and only renders when the current viewer is Owner or has canSeeFullGuide ON. Clicking the heading expands a secondary table of contents pointing to eight new hidden sections that document everything set up tonight: Supabase project setup (staging + prod URLs and keys), Cloudflare Pages hosting and the gb-bizcalc.dev + gb-bizcalc-staging.dev domain purchases, the hostname auto-routing rules, the Cloud tab and env override, the Owner offline emergency fallback, the offline password auto-sync behaviour, the 3-wrong-attempts Forgot password flow, and the schema / RLS / seed insert scripts. Each section has its own orange PDF download button, back-to-TOC link, and hidden anchor so you can link to them directly. (4) VERSION STAMP. Bumped from C5.6 → C5.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.7', AND the JSON export __version__ field now reads 'C5.7'. The CSV download _<VERSION> suffix automatically advances to "_C5.7". All prior changelog rows (C5.6 through B2.8) are preserved.
C5.6May 2026Three quality-of-life fixes on top of C5.5 plus the version bump. All three land safely on top of the Supabase Auth flow from C5.5 — nothing about how login normally works changes. (1) OFFLINE PASSWORD AUTO-SYNC. Every successful cloud sign-in now silently caches the password you just typed to the local users store, replacing whatever old value was there. Next time you are offline the Owner-emergency-fallback accepts your current Supabase password instead of forcing you to remember an older "Welcome1"-style starter password. If you change your Supabase password from the Users tab, the change already synced both ways in C5.5; this covers the case where you reset the password in the Supabase dashboard instead. (2) OWNER-ONLY ENVIRONMENT OVERRIDE. The Cloud tab now has a "Force environment" row with three buttons: Force STAGING, Force PROD, and Use hostname (default). Clicking one saves your choice to localStorage and reloads the app against that environment's Supabase project. A yellow "OVERRIDE ACTIVE" tag appears next to the env badge whenever a manual override is in effect, so you always know when you are pointing somewhere other than the hostname default. Perfect for testing the prod connection from your laptop before you deploy to the gb-bizcalc.dev domain. (3) FORGOT PASSWORD LINK ON LOGIN. After 3 wrong password attempts on a single login-overlay session, a "Forgot password?" link appears next to the error message. Clicking it calls supaClient().auth.resetPasswordForEmail(email) against whatever email you have typed into the Email field, so Supabase sends a password-reset link to that address. Reset links go to your Auth → Email Templates → "Reset Password" template on the corresponding project (staging or prod). This means self-service password recovery for anyone whose email is real and reachable — no more console-diving to unlock a forgotten password. (4) VERSION STAMP. Bumped from C5.5 → C5.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.6', AND the JSON export __version__ field now reads 'C5.6'. The CSV download _<VERSION> suffix automatically advances to "_C5.6". All prior changelog rows (C5.5 through B2.8) are preserved.
C5.5May 2026Phase 2 step 2 on top of C5.4 plus the version bump. Login flow now runs through Supabase Auth against the environment the browser routed to in C5.4 (staging or prod). Everything the app used to do locally is preserved as an offline fallback so the app still works with no internet. (1) CLOUD-BACKED LOGIN. Sign in on the login overlay now calls supaClient().auth.signInWithPassword against the routed Supabase project. On success the app pulls your profiles row (title, permissions, owner-flag) and mirrors it into the local users cache so the Users tab shows the same data across every browser. (2) OFFLINE OWNER FALLBACK. If the Supabase library never loaded (no internet on boot) or the auth request throws a network error, the login overlay falls back to the C5.4 local password check — but ONLY when the account signing in is the Owner (offline emergency access rule). Everyone else needs a working connection to sign in for the first time; after that their session token is cached and works offline until it expires. (3) PERSISTENT SESSIONS. Once you sign in the browser remembers the session for ~1 hour of activity and auto-refreshes silently in the background. Closing the tab and reopening it later on the same machine skips the login overlay entirely. (4) CLOUD SIGN-OUT. The account menu's Sign out button now calls supaClient().auth.signOut() before hiding the app — the session token is revoked on Supabase, so the next login requires a real password again. (5) PASSWORD CHANGE HITS SUPABASE. When you (or another user acting on their own account) change a password via the Users tab, the new password is pushed to Supabase Auth and updated in the local cache so offline login stays in sync. Owner changes to other users' passwords still only update the local cache for now — C5.6 will move those to a Supabase admin RPC. (6) LOGIN OVERLAY ENV BADGE. The login overlay now shows a small pill in the header reading "Cloud auth · STAGING" or "PROD" when the Supabase client loaded, or "Offline mode" if it did not. Handy confirmation of which environment you are about to sign into. (7) VERSION STAMP. Bumped from C5.4 → C5.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.5', AND the JSON export __version__ field now reads 'C5.5'. The CSV download _<VERSION> suffix automatically advances to "_C5.5". All prior changelog rows (C5.4 through B2.8) are preserved.
C5.4May 2026Phase 2 step 1 on top of C5.3 plus the version bump. Nothing existing was changed — only additions to plumb the cloud backend in. Login flow still uses the C4.8 local password check by default; Supabase Auth wiring switches on in step 2 once you confirm the SQL schema is loaded on both projects. (1) SUPABASE CLIENT LIBRARY LOADED. Added the official @supabase/supabase-js v2 CDN script to the head so window.supabase.createClient(url, key) is available on every page. Zero size impact on offline use — the script fails gracefully when there is no internet. (2) STAGING + PROD CREDENTIALS EMBEDDED. The Project URL and anon key pairs for gb-bizcalc-staging (agorpojlrycflemsvbgi.supabase.co) and gb-bizcalc-prod (gkzqomhvygzfkynnbybw.supabase.co) are constants in the phase-2 script block, gated behind a hostname router. (3) HOSTNAME AUTO-ROUTING. On boot the app looks at window.location.hostname and picks: production creds when the host contains gb-bizcalc.dev without the staging substring; staging creds for anything else — including localhost, file://, .pages.dev preview URLs, and any host containing staging. This means the identical HTML file works safely in every environment without a manual switch. (4) SETTINGS → CLOUD TAB. A new fourth tab reads "Cloud" and shows which environment the browser auto-selected (staging vs prod), the Supabase Project URL, a status pill (Connected / Not connected / Offline), and a "Test connection" button that does a minimal ping. Owner-only; other accounts see a static "Cloud sync is Owner-only" note. (5) NO USER-VISIBLE BEHAVIOR CHANGE YET. Login / auth / sync still runs 100% local. C5.5 will flip the login flow over once the SQL schema (delivered alongside this build) is applied in the Supabase SQL Editor on both projects and I confirm the test-connection button lights up Connected on your browser. (6) VERSION STAMP. Bumped from C5.3 → C5.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.4', AND the JSON export __version__ field now reads 'C5.4'. The CSV download _<VERSION> suffix automatically advances to "_C5.4". The C5.3, C5.2, C5.1, C5.0, C4.9, C4.8, C4.7, C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C5.3May 2026Three Phase-1 finalization fixes on top of C5.2 plus the version bump. This closes out Phase 1; Phase 2 (Supabase cloud sync) starts next. (1) ALT EMAIL SLOT APPEARS WHEN GRANTED. When the Owner turns on the "Alt email OK" pill on a user card, a second labeled "Alt email" input now appears directly below the primary email field on that same card so anyone (Owner and the user themselves once Phase 2 ships) can see the extra slot is active + fill it in. The primary email still enforces the @GripBlades.com rule; the alternate email is free-form. (2) TITLE LOCK IS AN OWNER TOOL BY DEFAULT. The Title locked pill no longer renders on every user card for every viewer. It now appears only on the Owner's screen (and on the screen of any account the Owner has granted the new "Can manage title lock" permission via a pill on that account's row). SandBox and any other ungranted account no longer sees the Title locked pill at all. (3) LOCKED TITLE SHOWS "LOCKED" INDICATOR + DISABLES THE DROPDOWN. When Title locked is ON and the viewing user is not the Owner and does not have the manage-title-lock grant, the Title field on that card renders as plain text (no dropdown) and the label to the right of the Title heading reads "Locked" in light-gray lettering so the viewer can see the lock is in force. Attempting to change the title from that account is now impossible from the UI. (4) VERSION STAMP. Bumped from C5.2 → C5.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.3', AND the JSON export __version__ field now reads 'C5.3'. The CSV download _<VERSION> suffix automatically advances to "_C5.3". The C5.2, C5.1, C5.0, C4.9, C4.8, C4.7, C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C5.2May 2026Two Phase-1 polish fixes on top of C5.1 plus the version bump. (1) OWNER PINNED, EVERYONE ELSE SORTED. The Users tab now always shows the Owner (you) at the top of the accounts list. Everyone else renders below the Owner in alphabetical order by last name (case-insensitive). Adding new accounts auto-inserts them into the correct sorted position — no more "whichever one I added most recently sits at the bottom." (2) TOGGLES REBUILT AS BULLETPROOF BUTTON PILLS. The four permission toggles (Title locked, Alt email OK, Use Owner login, Can assign Test Account title) no longer use native <input type="checkbox"> elements. Each is now a <button> pill with an explicit onclick handler that calls the toggle function directly. The button's ON/OFF state paints as a filled blue pill when on, a muted gray outline when off, so you can see the state without needing a checkbox glyph. This side-steps every checkbox-related browser quirk that has been blocking clicks in the previous attempts — buttons cannot be silently swallowed by layout, labels, or synthetic click handling. (3) VERSION STAMP. Bumped from C5.1 → C5.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.2', AND the JSON export __version__ field now reads 'C5.2'. The CSV download _<VERSION> suffix automatically advances to "_C5.2". The C5.1, C5.0, C4.9, C4.8, C4.7, C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C5.1May 2026Three Phase-1 refinements on top of C5.0 plus the version bump. (1) SETTINGS → USERS REORDERED. The Titles pool + "+ Add account" button now sit ABOVE the account cards so they are the first things you see when you open the tab (previously they were tucked away below a long list of accounts). (2) STRONG PASSWORD REQUIREMENT. New / changed passwords must be at least 10 characters, include at least 1 digit, and include at least 1 special character (anything that isn't a letter or digit). A small hint reading "10+ chars · 1 digit · 1 symbol" appears to the right of the "New password" label the moment you click Change on any account so you know exactly what the checker is looking for. Save is refused with an explicit error if any of the three checks fail. The C5.0 random-temp-password generator was upgraded to always emit a string that meets the same rules, so newly-created accounts pass on first sign-in without needing a change. (3) TOGGLE CLICKABILITY FIXED. The four checkbox toggles (Title locked, Alt email OK, Use Owner login, Can assign Test Account title) now use direct onclick handlers with an inline this.checked pass so every browser / webview fires them reliably. CSS was tightened too with pointer-events:auto !important and an explicit larger hit target on the input, so the checkbox is always clickable regardless of the surrounding flex layout. (4) VERSION STAMP. Bumped from C5.0 → C5.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.1', AND the JSON export __version__ field now reads 'C5.1'. The CSV download _<VERSION> suffix automatically advances to "_C5.1". The C5.0, C4.9, C4.8, C4.7, C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C5.0May 2026Five Phase-1 hardening fixes on top of C4.9 plus the version bump. All local; no cloud yet. (1) OWNER CAN NO LONGER IMPERSONATE. The identity pill dropdown no longer lets you jump into another account without their password. It now shows just the current user, a Manage Users… entry, and Sign out. To view another user's activity you go to Settings → Audit Log (Owner sees everyone by design); to actually work as that user you must sign out and sign in with their password. (2) NO GHOST PASSWORD HINT. The password fields on the login overlay and in the Settings → Users cards no longer show "Welcome1" as ghost placeholder text — the placeholder is blank so nobody reads the default off the screen. (3) PASSWORD-CHANGE CONFIRMATION FLOW. Editing a password in Settings → Users now opens a two-field change panel: current password stays masked and read-only, click "Change" to reveal "New password" + "Confirm new password" boxes plus Save/Cancel buttons. Save only succeeds when the two fields match and are non-empty; on success the change UI collapses back to the read-only masked field. Cancel discards. This prevents a single typo from locking anyone out. (4) TITLE-LOCK CHECKBOXES ARE CLICKABLE. The C4.8 wiring depended on querySelector-attached change handlers that weren't always binding in every browser. All user-field toggles (Title locked, Alt email OK, Use Owner login, Can assign Test Account title) now use direct inline onchange handlers that call the new global gbUpdateUserField, so toggling always writes to storage. (5) NEW ACCOUNT ADDER + RANDOM TEMP PASSWORD. Settings → Users has a new "+ Add account" button below the cards. Clicking it prompts for the new user's name + email (@GripBlades.com by default), generates a 10-character random temporary password (letters + digits, avoiding easily-confused chars), and pops a modal with the credential + a note that Phase 2 will auto-email this to the address on file. Seeded accounts (mg, jj, sandbox) keep their existing passwords so nobody is locked out mid-session. (6) VERSION STAMP. Bumped from C4.9 → C5.0 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C5.0', AND the JSON export __version__ field now reads 'C5.0'. The CSV download _<VERSION> suffix automatically advances to "_C5.0". The C4.9, C4.8, C4.7, C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.9May 2026One migration fix on top of C4.8 plus the version bump. (1) LOGIN BACKFILL FIX. Accounts that were seeded on C4.6 (before email/password fields existed) were being loaded as-is with password === undefined when C4.7 added those fields. That made the login check compare the typed "Welcome1" against an empty string, so every seed account was locked out. C4.9's usersLoad() now runs a per-user backfill: missing email, password, allowAltEmail, canAssignTestTitle, and useOwnerLogin fields are refilled with the C4.7 defaults on load. Existing values you already changed (name, title, custom email) are preserved. If you already sat at the C4.8 login stuck at "Incorrect password", a hard-refresh (Cmd/Ctrl+Shift+R) after loading C4.9 will let you in with Welcome1. (2) VERSION STAMP. Bumped from C4.8 → C4.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.9', AND the JSON export __version__ field now reads 'C4.9'. The CSV download _<VERSION> suffix automatically advances to "_C4.9". The C4.8, C4.7, C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.8May 2026Three follow-up polish fixes on top of C4.7 plus the version bump. (1) USERS PANEL REDESIGNED AS CARDS. The cramped 8-column table has been replaced with a stacked card layout — one card per account with labeled fields, clear spacing, and full-width inputs. Every value (Name, Initials preview, Title, Email, Password, plus the various permission toggles) is readable at a glance. The Owner-only columns that were unlabeled at the right edge now carry proper "Alt email OK", "Use Owner login", and "Can assign Test" labels next to each checkbox. (2) SIGN OUT + LOGIN FLOW. The identity pill's dropdown now has a "Sign out" entry at the bottom. Clicking it drops you into a login overlay that asks for account + password before you can use the app. The overlay respects the C4.7 "Use Owner login" toggle: Test-Account rows with the toggle ON accept the current Owner's password instead of their own so you don't have to context-switch when testing permission gates. All password checks happen locally against the stored default (Welcome1) or whatever you set in Settings → Users. Real Supabase-backed auth still lands in Phase 2; this is the local groundwork so the flow is real. (3) CHANGE LOG BUTTON NEXT TO CLOSE. The Change Log button in the CUST INFO popup now hugs the × close button (small gap) instead of sitting alone in the middle of the head. The head's space-between layout was letting a 3rd flex item drift; a margin-left: auto on the button pins it next to the close ×. (4) VERSION STAMP. Bumped from C4.7 → C4.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.8', AND the JSON export __version__ field now reads 'C4.8'. The CSV download _<VERSION> suffix automatically advances to "_C4.8". The C4.7, C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.7May 2026Seven Phase-1 polish fixes on top of C4.6 plus the version bump. Nothing existing was changed — only additions and small refinements to the C4.6 identity + audit + backup work. (1) IDENTITY WIDGET SHOWS INITIALS. The identity pill above the GripBlades logo now displays the current user's first-initial + last-initial (e.g. "Mark Grote" renders as "MG"; "Jason Jett" renders as "JJ"; "Sand Box" renders as "SB"). The full name still shows in the Settings → Users table and inside the account-switcher dropdown. (2) OWNER NAME IS NOW EDITABLE. The Owner account's name field is no longer read-only. Default seed changed from "MG" to "Mark Grote". (3) "SAND BOX" RENAMED (was "SandBox") and its default title changed to "Test Account". Initials now abbreviate to "SB". (4) NEW "TEST ACCOUNT" TITLE. Added as a permanent default. Only Owner (or an account the Owner explicitly grants the Assign Test Account title permission to) may set an account's title to "Test Account". (5) EMAIL + PASSWORD FIELDS. The Users table now shows an Email column (defaults to <initials>@GripBlades.com) and a Password column (defaults to "Welcome1" for every seeded and freshly-added account). Email input requires an @GripBlades.com address by default; the Owner can toggle a per-account "alternate email allowed" checkbox to let a user register any address. Real password-based login lands with the cloud in Phase 2 — the fields shipped in C4.7 are the storage + UI groundwork. (6) "TEST ACCOUNTS LOG IN WITH MY ACCOUNT" TOGGLE. For any account whose title is "Test Account", the Users table shows a new per-row toggle labeled "Log in with my account". Enabled by default. When on, switching into that test account uses the Owner's credentials so no password re-entry is needed while you test permission gates. The toggle only appears on Test-Account rows. (7) CHANGE LOG BUTTON NOW APPEARS ON CUST INFO. The C4.6 button-injection hook was looking for a class name that doesn't match this app's CUST INFO markup (.roi-cinfo-modal / .roi-cinfo-head). C4.7 rewires the hook to the correct selectors AND wraps the existing roiOpenCustomerInfoPanel() function so the button is guaranteed to appear on every open, tied to the exact card id being viewed. (8) VERSION STAMP. Bumped from C4.6 → C4.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.7', AND the JSON export __version__ field now reads 'C4.7'. The CSV download _<VERSION> suffix automatically advances to "_C4.7". The C4.6, C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.6May 2026Phase 1 of the multi-user rollout on top of C4.5 plus the version bump. Everything in this release is LOCAL only — no cloud sync yet (that arrives in Phase 2). Nothing from C4.5 is removed or altered; every existing storage key, Customer Card, LOG entry, favorite, and setting still works exactly as before. (1) IDENTITY WIDGET. A small person-icon + name + title readout has been added directly above the GripBlades logo. Click the widget to open a mini account switcher that lets you pick between the seeded accounts. When Phase 2 adds real login this widget becomes the login/logout entry point and password panel. (2) SEEDED ACCOUNTS. Three default accounts ship: "MG" (Owner, title locked to 'Owner'), "Jason Jett" (Sales Lead), and "SandBox" (unassigned title). You are MG. SandBox is a dummy account you can use to test permission gates once Phase 3 lands. (3) TITLES SYSTEM. Ten default titles are permanent: Owner, Sales, Sales Lead, Customer Service, Purchasing, Shipping, Operations, Warehouse, Marketing, and Accounting. Add your own titles via the + button in the Settings → Users panel; remove any custom title with the − button (the defaults are permanent). Each account has a title-locked toggle so the owner can force a specific title or let the user pick their own. (4) AUDIT LOG. A new local audit log records every write to the app's data storage keys (Customer Cards, per-basis LANDED values, per-basis logged history, notes, sub-profile locks and basis-locks, Build Notes / Leads text). Each entry captures who / when / which key / what changed. Owner sees every user's activity; each non-owner sees only their own. Live-viewed in the Settings → Audit Log tab with per-user and per-key filters. (5) CUST INFO CHANGE LOG. Each Customer Card's CUST INFO popup gets a new "Change Log" button to the left of the × close button. Clicking it shows the audit-filtered history for that specific card. Each row has a "Changeback" button that restores the "before" value; a single confirm is required for name / notes / lock-state fields, and a second "Are you 100% sure?" confirm is required for Cost Basis + 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC data. (6) BACKUP PANEL. Settings → Backup replaces the hyperlink-style Save Data / Load Data pair with two proper buttons. Save Data still exports the full JSON snapshot exactly as it does today (BackUp_GBbizCalc_<MMDDYY><letter>.json). Load Data now asks first — "Upload Full Backup - Overwrite All Data" or "Upload Partial Backup - Choose Specific Data" — and the partial path lets you tick the exact keys / sub-keys you want to restore (Customer Card tree, individual cards, individual profiles, individual bases, and per-pack data all pickable). (7) VERSION STAMP. Bumped from C4.5 → C4.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.6', AND the JSON export __version__ field now reads 'C4.6'. The CSV download _<VERSION> suffix automatically advances to "_C4.6". The C4.5, C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.5May 2026Two feature updates on top of C4.4 plus the version bump. (1) DUAL-TAB LEADS + BUILD NOTES. Both the LEADS popup and the BUILD NOTES popup now open with a two-tab header at the top. The left tab defaults to "MG's Leads" / "MG's Build Notes"; the right tab defaults to "JJ's Leads" / "JJ's Build Notes". Only one tab is active at a time; the other tab is grayed but still visible so you can see which slot exists for the other person. Whichever tab is active fills the full popup body so a human eye can read the maximum amount of data. Double-click a tab label to rename it via a prompt dialog (so "MG" and "JJ" can become anyone's initials or a full name). The currently-active tab is remembered per popup, so closing and reopening lands on the same tab you left. Existing BUILD NOTES / LEADS text is one-time-migrated into the LEFT (MG) tab so nothing you already typed is lost. Storage keys: gbBizCalcBuildNotesTabs_v1 and gbBizCalcLeadsTabs_v1. (2) BROWN COST BASIS ONLY WHEN LOCKED PROFILE IS ACTIVE. The light-brown fill + dark-brown 2px outline treatment on a MACRO COST BASIS chip that has a sub-profile locked to it no longer paints unless the chip is the currently-selected basis. When it is not selected (the user is on a different cost basis) the chip falls through to its normal look. The always-visible faded padlock badge (P1 / P2 / P3) stays put as the persistent lock indicator; only the color treatment became conditional. Combined with C4.4 's bold-when-selected rule, the active basis is now visually unambiguous even on a fully-locked bar. (3) VERSION STAMP. Bumped from C4.4 → C4.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.5', AND the JSON export __version__ field now reads 'C4.5'. The CSV download _<VERSION> suffix automatically advances to "_C4.5". The C4.4, C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.4May 2026One clarity fix on top of C4.3 plus the version bump. (1) SELECTED COST BASIS ALWAYS BOLD, NEVER THE LAST-LOCKED. The currently-selected MACRO COST BASIS chip in the top bar now renders its name in a heavier weight (800) than the other chips (500). Previously the difference between selected (font-weight 600) and non-selected (500) was subtle, and combined with the brown-locked treatment on multiple chips it could look like the "last profile locked" chip was bolder than the active one, which mis-signaled which cost basis was actually being edited. Both the standard selected chip AND the locked selected chip now explicitly get font-weight:800; every other chip — locked or not — is pinned to font-weight:500 so the bold indicator always tracks the active basis. (2) VERSION STAMP. Bumped from C4.3 → C4.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.4', AND the JSON export __version__ field now reads 'C4.4'. The CSV download _<VERSION> suffix automatically advances to "_C4.4". The C4.3, C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.3May 2026Two follow-up fixes on top of C4.2 plus the version bump. (1) INACTIVE LOCKED PROFILES USE DEFAULT STYLING. A leftover C3.9-era CSS block was applying the brown-locked treatment to every .roi-c39-basislocked P-button and PROFILE tab REGARDLESS of whether they were the currently-active profile. That block has been tightened to require .active / .is-active so inactive locked P1 / P2 / P3 buttons and inactive locked PROFILE 1 / 2 / 3 tabs now render in their standard state (light-blue has-data pill with dark-blue rim, or empty white pill) — matching how unlocked P-buttons look when they are not the active profile. Only the active locked profile shows brown-with-white-text. (2) HAS-DATA DOT WHITE ON BROWN. When a P-button IS active + basis-locked, the small 5px dot to the right of the P1 / P2 / P3 letter now renders SOLID WHITE against the brown background so it stays visible. A triple-class-stack override was added so the white color always wins over any downstream rule. (3) VERSION STAMP. Bumped from C4.2 → C4.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.3', AND the JSON export __version__ field now reads 'C4.3'. The CSV download _<VERSION> suffix automatically advances to "_C4.3". The C4.2, C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.2May 2026Four follow-up fixes on top of C4.1 plus the version bump. (1) CLICK-ALERT ON LOCKED PANEL. Clicking any writable input (10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC price fields, promo buttons, the MACRO COST BASIS Landed $/pc input, its Log button, and click-to-edit margin percentages) while the active sub-profile is locked to a different cost basis now pops a centered alert reading "Locked to Cost Basis XXX - Go to XXX Cost Basis to Edit Data" where XXX is the 3-letter abbreviation of the profile's locked basis. Inputs stay visually grayed-out AND click-blocked. (2) HIDE HIDES LOCK VISUALS. Clicking a HIDE button now also collapses the brown-locked treatment on the MACRO COST BASIS chip (native accent-blue selected look returns) and hides the small "Locked-XXX" readout under each PROFILE tab, restoring the panels to their pre-C3.9 HIDE appearance. The lock state itself is not touched — clicking SHOW brings the brown treatment and Locked-XXX hint back. (3) NOTES-TOGGLE READ-ONLY BUG FIXED. Opening + closing the per-profile Notes textarea used to leave the panel inputs writable even when the profile was basis-locked (because _roiRenderCardInner re-created the DOM without re-applying the disabled state). roiUpdateInputsDisabled now runs after _roiRenderCardInner AND after _roiRenderAllVisible so the basis-lock read-only state survives every panel repaint. (4) BROWN ONLY WHEN SELECTED. Basis-locked P1 / P2 / P3 buttons (scroller + ghost) AND the PROFILE 1 / 2 / 3 tabs now render brown-with-white-text ONLY when they are the active/selected profile. Inactive locked profiles fall through to the standard styling (light-blue has-data pill or white empty pill with the darker-blue rim) just like the unlocked P-buttons do — the padlock badge on the locked cost basis chip in the MACRO COST BASIS bar remains the always-visible indicator of which profile is locked to each basis. (5) VERSION STAMP. Bumped from C4.1 → C4.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.2', AND the JSON export __version__ field now reads 'C4.2'. The CSV download _<VERSION> suffix automatically advances to "_C4.2". The C4.1, C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.1May 2026Four follow-up fixes on top of C4.0 plus the version bump. (1) BASIS-LOCKED PROFILES ARE READ-ONLY OFF-BASIS. When a profile is locked to a cost basis (say P2 → FEU), you can now freely switch to that profile from any other cost basis and inspect its values — but every writable input (10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panels, promo buttons, and the MACRO COST BASIS LANDED $/pc input) is disabled unless the current cost basis matches the profile's locked basis. To edit, switch to the matching cost basis (in this example FEU). Hover a disabled input to see "Profile is Locked-XXX — switch to the XXX Cost Basis to edit." The old C4.0 "Go to XXX Cost Basis?" confirm on the click path is removed — clicking a basis-locked profile always switches directly with no popup. (2) SCROLLER P-BUTTON STAYS BROWN. Added an all-inclusive higher-specificity CSS rule that guarantees the brown-with-white-text override wins against the .has-data / .has-custom-color / .active per-sid rules on the P1 / P2 / P3 buttons in the CUSTOMER CARDS scroller and CUSTOMER CARD CLIPBOARD ghost slots. Applies whether the button is currently the active profile or not, and roiRenderCustomerCards is called defensively after every cost-basis switch so no stale class ever survives past a repaint. (3) UPPERCASE MENU LABELS. The right-click menu row now reads "LOCK TO COST BASIS" when unlocked and "UNLOCK FROM XXX" when locked, matching the formatting of every other menu row (LOCK PROFILE X, CLEAR PROFILE X, CO. APPROVED RATES, SUBMITTED FOR REVIEW). The 3-letter submenu abbreviations were already uppercase and are unchanged. (4) HINT-PLACEHOLDER WIDTH PARITY. Fixed the horizontal shift where locking a profile appeared to nudge the button sideways. The invisible "Locked-XXX" placeholder under each PROFILE tab now uses the SAME 10-character width as an actual locked label (Locked-PAL / Locked-TEU / Locked-FEU) in the mono font, so no cell grows or shrinks when its lock state changes and all three tabs stay horizontally aligned. (5) VERSION STAMP. Bumped from C4.0 → C4.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.1', AND the JSON export __version__ field now reads 'C4.1'. The CSV download _<VERSION> suffix automatically advances to "_C4.1". The C4.0, C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C4.0May 2026Seven polish fixes on top of C3.9 plus the version bump. (1) COST BASIS ABBREVIATION. _roiBasisAbbrevC39 now strips the parenthesis CHARACTERS only (keeps their content), then takes the first 3 alphanumeric characters and uppercases. So the "(FEU) 40 Ft. Cost Basis" label now abbreviates to "FEU" (was incorrectly clipping to "40F") and the "(TEU) 20 Ft. Cost Basis" label abbreviates to "TEU" (was "20F"). Applies everywhere: right-click menu ("Unlock from FEU" now reads all three letters), the small "Locked-XXX" hint under each PROFILE tab, the padlock-badge letter, and the "Go to XXX Cost Basis?" confirm popup. (2) "Locked to XXX" renamed to "Locked-XXX" (dash, no space) so the readout fits on one line under the PROFILE 1 / 2 / 3 tab without wrapping. (3) VERTICAL ALIGNMENT. Every PROFILE 1 / 2 / 3 tab cell now reserves the space for the "Locked-XXX" hint at render time, so a locked profile no longer shifts its button up. All three tabs stay perfectly aligned horizontally whether or not any of them is locked. (4) BROWN-LOCKED STYLE OVERRIDES ACTIVE STATE. Basis-locked brown styling now wins over the per-sid active-blue / active-red / active-purple rules on the P-buttons, so a locked profile shows brown-with-white-text immediately when you switch cost basis (not just after you click off + back on). (5) HAS-DATA DOT COLOR. The 5px dot in a locked P-button's has-data indicator now renders WHITE against the brown background (was blue). (6) "GO TO XXX COST BASIS?" CONFIRM. Clicking a locked profile while on a different cost basis now pops a centered confirm dialog reading "Go to PAL Cost Basis?" (or TEU / FEU / etc.). YES jumps to that cost basis and selects the locked profile so you can edit it; NO leaves you exactly where you were. The old auto-jump behavior is gone; the switch always requires explicit consent. (7) UNLOCK MENU ABBREVIATION. Combined with fix (1), the right-click "Unlock from XXX" menu row now shows the full 3-letter abbreviation instead of just the first letter. (8) VERSION STAMP. Bumped from C3.9 → C4.0 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C4.0', AND the JSON export __version__ field now reads 'C4.0'. The CSV download _<VERSION> suffix automatically advances to "_C4.0". The C3.9, C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.9May 2026Two feature changes on top of C3.8 plus the version bump. (1) SAVE DATA FILENAME RENAMED. The Save Data JSON download filename convention has changed from "gb-bcalc-MM-DD-YY[letters].json" to "BackUp_GBbizCalc_MMDDYY[letters].json". The trailing a / b / c per-day letter suffix (bijective base-26) is preserved so multiple same-day saves still line up in download order. Load Data accepts BOTH the new C3.9 name and the legacy C3.8- filename patterns, so backups you already have on disk keep loading fine. (2) LOCK PROFILE TO COST BASIS. Right-click any P1 / P2 / P3 short-name in the CUSTOMER CARDS scroller (or ghost CUSTOMER CARD CLIPBOARD) OR any PROFILE 1 / 2 / 3 long-name tab in a 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panel to see a new top-of-menu "Lock to Cost Basis" entry with a hover-triggered submenu listing every current MACRO COST BASIS — PAL / TEU / FEU (built-ins) plus any custom bases you have added, in the same left-to-right order they appear in the MACRO COST BASIS bar. Each entry is shown as its 3-letter UPPERCASE abbreviation (parenthesized text stripped first, then first 3 letters). Clicking one locks that sub-profile to that basis for this customer card. Locked-state effects: (a) the menu entry flips to "Unlock from XXX" on a brown background with white text; (b) both the P-button (scroller + clipboard) and the PROFILE 1 / 2 / 3 long-name tab render brown-with-white-text so you can see at a glance which profile is locked; (c) the locked cost basis in the MACRO COST BASIS bar renders with a dark-brown 2px outline + very-light-brown interior instead of the usual accent-blue treatment when selected, and shows a small faded, non-clickable padlock with the profile letter (P1 / P2 / P3) vertically centered at its top-left; (d) a small "Locked to XXX" readout in the same font size + text3 color as the "UPC: ..." label appears directly under the PROFILE 1 / 2 / 3 tab. (e) clicking the locked cost basis in the MACRO COST BASIS bar auto-jumps to the locked profile; clicking the locked profile auto-jumps to the locked cost basis. (f) clicking any OTHER profile button on a customer card whose active basis is locked to a different profile is refused with a centered alert so the visible edit surface always matches the visible cost basis. Each (card, sub) supports one lock; each (card, basis) supports one lock. Lock state is persisted at gbBizCalcRoiSubBasisLock_v1 in localStorage. (3) VERSION STAMP. Bumped from C3.8 → C3.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.9', AND the JSON export __version__ field now reads 'C3.9'. The CSV download _<VERSION> suffix automatically advances to "_C3.9". The C3.8, C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.8May 2026Two TariffNews popup fixes on top of C3.7 plus the version bump. (1) News grid thumbnails now sort NEWEST FIRST by article date (with time of day when the parsed date carries it). The 2-column CSS grid reads left-to-right, top-to-bottom — newest at top-left, 2nd newest at top-right, 3rd at row 2 left, 4th at row 2 right, and so on. Sort key is Date.parse(item.date) with __fetchedAt as fallback when a date string is unparseable. (2) Double-click any subject identifier badge (ESCALATION / NEGOTIATION / etc.) in the top-left of a thumbnail to filter the grid to only articles carrying that same identifier. The filtered view is sorted newest-first the same way. Double-click any identifier again (same tag or any other) to clear the filter and return to the default all-articles view. Single-click on a tag does nothing so the double-click intent is preserved (you can still click anywhere else on the thumbnail to open the article). (3) VERSION STAMP. Bumped from C3.7 → C3.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.8', AND the JSON export __version__ field now reads 'C3.8'. The CSV download _<VERSION> suffix automatically advances to "_C3.8". The C3.7, C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.7May 2026One math bug fix on top of C3.6 plus the version bump. (1) Pre-Paid Ship-Calc — MFR SELL PRICE SOURCE. Fixed the "WAS - $X.XX" margin in the Pre-Paid Ship-Calc tab reading the wrong sell price on the per-MC-order card. Before this fix, shipCalcRefresh() read priceRec from the MC SKU record (stateByBasis[mcSku.mpn]) whose s1_sell / s2_sell / s3_sell fields are stored INDEPENDENTLY from the base 10 Pack / 40 Pack SKU records. A user who typed "MFR Sell Price (Direct to Retail) = $5.49" on the 10 Pack panel would still see Ship-Calc compute against a stale MC-panel value (e.g. $9.99/pack × 120 = $1,198.80/MC, producing an original margin of $1,198.80 − $456.84 = $741.96), not the expected $658.80/MC ($5.49 × 120) − $456.84 = $201.96 original margin. Ship-Calc now derives priceMC by reading s${N}_sell from the BASE SKU record and multiplying by ROI_PACKS_PER_MC[pack] on the fly, so the ship-cost chain cascades correctly from cost-per-blade → per-pack → per-MC → per-order and matches ROI-Calc's LANDED / N PK MC readouts. Promo % values are also now read off the base SKU record for consistency (promos already lived per-basis; the sourcing side just matches the sell price). (2) VERSION STAMP. Bumped from C3.6 → C3.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.7', AND the JSON export __version__ field now reads 'C3.7'. The CSV download _<VERSION> suffix automatically advances to "_C3.7". The C3.6, C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.6May 2026Two PDF-side polish fixes on top of C3.5 plus the version bump. (1) FULL variant MACRO COST BASIS PDF header banner is now painted BARN RED (#7C0A02) instead of the previous near-black #1c1c1e. DIST. Ed. variant keeps the black banner so the two variants are visually distinct at a glance. (2) FULL variant heading now appends " - SHOWS LANDED COSTS" after the basis label in the banner subtitle. For example: "ROI-Calc — PALLET BASIS - SHOWS LANDED COSTS". DIST. Ed. variant subtitle is unchanged (still appends " — DIST. Ed."). (3) VERSION STAMP. Bumped from C3.5 → C3.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.6', AND the JSON export __version__ field now reads 'C3.6'. The CSV download _<VERSION> suffix automatically advances to "_C3.6". The C3.5, C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.5May 2026Four fixes on top of C3.4 plus the version bump. (1) HIDE compresses the panel upward. Clicking HIDE now collapses the LANDED COST READOUT block (Landed / Piece, Landed / N Pack, Landed / N Pk MC, Landed / Pallet) so the PROFILE 1 / 2 / 3 tab row + Notes button slide up into the space the readout used to occupy. The pack heading and its thin gray rule remain in place. Clicking SHOW expands the readout back to its full height. (2) LOG popup row buttons. Each row in the PALLET / TEU / FEU COST BASIS — LOGGED VALUES popup now carries two buttons: DELETE (pops the existing "CONFIRM TO DELETE THIS LOG" confirm) and CLICK/SWAP (pops "CONFIRM TO CHANGE THE $X.XXXX VALUE TO $Y.YYYY" showing the current active LANDED $/pc and the row's value). The legacy row-level right-click context menu is removed since both actions are now visible button affordances. (3) LAST DELETED LOGS. A hyperlink-style button sits to the LEFT of the 10-Pack SUBTAB label inside the LOG popup header. Clicking it expands a panel that shows the last 6 deleted log rows for this basis + pack subtab. Each deleted row carries a RESTORE button (same pattern as the SHIPPING REFERENCE Last Deleted History) which puts the row back at the top of the active log list. Restoration opens it back up for CLICK/SWAP from there. (4) Active-log scroll cap. The active LOG popup body now sizes to ~6 rows before scrolling; older rows are accessed by scrolling down inside the popup. The 10-row storage cap (ROI_BASIS_LOG_MAX) is unchanged — only the visible viewport height was reduced. (5) VERSION STAMP. Bumped from C3.4 → C3.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.5', AND the JSON export __version__ field now reads 'C3.5'. The CSV download _<VERSION> suffix automatically advances to "_C3.5". The C3.4, C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.4May 2026Three PDF-side fixes on top of C3.3 plus the version bump. (1) MACRO COST BASIS PDFs now reflect the active profile's lock mode in the report scope. When the exported profile is in CO. APPROVED RATES or SUBMITTED FOR REVIEW, the small "MACRO COST BASIS" heading (FULL variant) or "REPORT SCOPE" heading (DIST. Ed. variant) is followed by the mode label in parentheses — "- (CO. APPROVED RATES)" or "- (SUBMITTED FOR REVIEW)" — rendered in the same 9pt font, non-bold, in hyperlink blue. Profiles in normal mode print no suffix. (2) Header tag rename. The Distributor edition tag in the PDF header banner now reads "DIST. Ed." instead of the longer "DISTRIBUTOR EDITION". The PDF filename suffix (-dist) is unchanged. (3) Italic preamble removed. The line "Distributor edition — shows MSRP / DIST pricing and distributor margins only." that appeared under the REPORT SCOPE heading in DIST. Ed. PDFs is removed. The header tag and the REPORT SCOPE heading already communicate the variant unambiguously. (4) VERSION STAMP. Bumped from C3.3 → C3.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.4', AND the JSON export __version__ field now reads 'C3.4'. The CSV download _<VERSION> suffix automatically advances to "_C3.4". The C3.3, C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.3May 2026Two fixes on top of C3.2 plus the version bump. (1) Final residual lag on the P1 → P2 / P3 switch is eliminated by replacing the JSON.parse(JSON.stringify(...)) deep-clones in roiMirrorVisibleToActive with shallow Object.assign({}, ...) copies. Isolation is still preserved because roiLoadActiveIntoVisible immediately re-assigns the three roiState[basis] / roiPromos[basis] keys to brand new empty objects — the previous basis maps are no longer reachable through roiState and remain exclusively owned by the sub-profile snapshot. Switching from any profile (including a rich P1) is now effectively instantaneous. (2) HIDE button state persists across pack-size subtab switches. Previously a Hide click on the 10 PACK ROI panel would clear when the user switched to 40 PACK ROI (and vice-versa). The hidden state is now tracked in a module-scope flag and re-applied to every .roi-card on every panel render, so toggling Hide on the 10 PACK panel keeps every panel hidden across pack-subtab and MC-sibling navigation until any panel's Hide button is clicked again to toggle it off. (3) VERSION STAMP. Bumped from C3.2 → C3.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.3', AND the JSON export __version__ field now reads 'C3.3'. The CSV download _<VERSION> suffix automatically advances to "_C3.3". The C3.2, C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.2May 2026Two fixes on top of C3.1 plus the version bump. (1) Ghost CUSTOMER CARD CLIPBOARD now supports full drag-and-drop for its P1 / P2 / P3 buttons. Profiles in the ghost slots can be dragged onto other profiles (scroller or ghost) AND profiles from anywhere can be dropped INTO ghost-slot profiles — full parity with the scroller. Lock-target refusal still applies (LOCK PROFILE / CO. APPROVED RATES / SUBMITTED FOR REVIEW), and the orange drop glow continues to suppress on locked targets. (2) Profile switch lag (P1 → P2 / P3 on rich cards like ACE HARDWARE) is fixed by deferring the three localStorage writes — roiCardDataSave, roiStateSave, and roiPromosSave — through a coalesced setTimeout(0) queue. The deep-clone + DOM repaint complete on the click event itself; the serialization to localStorage runs on the next tick after the UI has already updated. Successive switches collapse into a single deferred save instead of three back-to-back synchronous writes. Identical behavior and data integrity — only the JS task scheduling changed. P2 → P3 was already fast because P2 / P3 were near-empty; now P1 → P2 / P3 matches that speed on cards of any size. (3) VERSION STAMP. Bumped from C3.1 → C3.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.2', AND the JSON export __version__ field now reads 'C3.2'. The CSV download _<VERSION> suffix automatically advances to "_C3.2". The C3.1, C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.1May 2026Three fixes on top of C3.0 plus the version bump. (1) Ghost CUSTOMER CARD CLIPBOARD P-buttons are now clickable. Clicking P1 / P2 / P3 inside a ghost slot now selects that customer-card + sub-profile and shows its panels — full parity with the left-to-right scroller. Ghost slots also reflect the same aesthetic state (lock icons, review / approved styling) as the parent card. (2) Visible padlock when LOCK PROFILE is active on an inactive P-button. Previously the solid-white padlock rendered onto the default white button background so the icon vanished. The padlock now renders in the same medium gray as the inactive P1 / P2 / P3 letter text whenever the button is inactive AND the lock state is LOCK PROFILE. When the button is the actively-selected profile (blue / red / purple BG) the padlock stays solid white. Review (black BG) keeps a white padlock; CO. APPROVED RATES (turquoise BG) keeps a black padlock — both unchanged. Applies identically to the scroller and the ghost CUSTOMER CARD CLIPBOARD. (3) Orange drag-drop glow no longer falsely appears over locked drop targets. Dragging a profile over a target profile that is in CO. APPROVED RATES, SUBMITTED FOR REVIEW, or LOCK PROFILE no longer paints the orange glow around the target button. The drop was already refused by roiSubDrop in C2.9; now the visual hint matches the actual behavior. Drag visuals over unlocked targets are unchanged. (4) VERSION STAMP. Bumped from C3.0 → C3.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.1', AND the JSON export __version__ field now reads 'C3.1'. The CSV download _<VERSION> suffix automatically advances to "_C3.1". The C3.0, C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C3.0May 2026Three fixes on top of C2.9 plus the version bump. (1) PDF section divider spacing. The thin black rule underneath the pack-type headings ("10 PACK / 10 PACK MC / 40 PACK / 40 PACK MC — SKU/MPN ...") in the per-basis PDF exports has been raised so it now sits roughly halfway between the pack-type heading and the next sub-heading (LANDED COST READOUT / PROFILE N. SECTION 1), leaving visible whitespace above and below the line. The rule no longer intersects with the sub-heading text on the line directly below it. (2) Approved P-button text is ALWAYS black. When a profile is in CO. APPROVED RATES mode, its short name (P1 / P2 / P3) now renders in BLACK text whether it is the active profile or not. Applies in both the CUSTOMER CARDS left-to-right scroller and the CUSTOMER CARD CLIPBOARD ghost slots. The long name (PROFILE 1 / 2 / 3) in the panels was already painted black against the turquoise background since C2.9 #4 — the short name now matches. (3) Cleared stale custom sub-profile colors. One-time cleanup wipes any leftover per-profile custom colors (such as the purple Orgill P1 left over from before the horizontal color bar was removed). After this migration runs once, every P1 / P2 / P3 button across every Customer Card returns to the standard styling: light blue when the profile has data, white when it is empty. The migration is gated by a localStorage flag (gbBizCalcRoiC30SubColorWipeDone) so it runs exactly once per browser. Future right-click color picks are still respected. (4) VERSION STAMP. Bumped from C2.9 → C3.0 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C3.0', AND the JSON export __version__ field now reads 'C3.0'. The CSV download _<VERSION> suffix automatically advances to "_C3.0". The C2.9, C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.9May 2026Ten fixes on top of C2.8 plus the version bump. (1) Only ONE padlock icon EVER shows next to a sub-profile short name (P1 / P2 / P3) in the CUSTOMER CARDS scroller and in the CUSTOMER CARD CLIPBOARD ghost slots. The three lock states — CO. APPROVED RATES, SUBMITTED FOR REVIEW, LOCK PROFILE — are mutually exclusive. (2) State precedence. Activating CO. APPROVED RATES or SUBMITTED FOR REVIEW on a profile that was already in LOCK PROFILE silently overrides LOCK PROFILE (no popup). Trying to activate LOCK PROFILE while a profile is in CO. APPROVED RATES or SUBMITTED FOR REVIEW pops a centered alert — "DE-ACTIVATE - CO. APPROVED RATES - TO CHANGE STATE" or "DE-ACTIVATE - SUBMITTED FOR REVIEW - TO CHANGE STATE" — and the LOCK PROFILE request is refused. (3) Padlock visuals. Padlocks are now SOLID-FILLED (not outlined). On the default white / colored / black backgrounds they are SOLID WHITE; on the turquoise CO. APPROVED RATES background they are SOLID BLACK so they stand out clearly. (4) Black text on approved turquoise. When CO. APPROVED RATES is active, the profile's short name (P1 / P2 / P3) AND its long name (PROFILE 1 / 2 / 3) both render in BLACK text against the turquoise background for high contrast. (5) Panel padlock now tracks LOCK PROFILE state in all four activation paths: right-click PROFILE long name in the panels, right-click P1 / P2 / P3 short name in the scroller, right-click P1 / P2 / P3 short name in the CUSTOMER CARD CLIPBOARD ghost slot, AND click the gray-padlock icon above the PROFILE long name in the panels. (6) Panel padlock click rules. Clicking the panel padlock toggles LOCK PROFILE on/off. If the profile is already in CO. APPROVED RATES or SUBMITTED FOR REVIEW, the click is a no-op (no second padlock is added). To exit CO. APPROVED RATES / SUBMITTED FOR REVIEW you must right-click the profile name and reselect the active state to deactivate. (7) Edit-block when locked. While any of the three lock states is active, ALL MACRO COST BASIS inputs are read-only. PDF download is still allowed; LOG popups remain viewable; no log entries can be selected, deleted, or favorited while locked. (8) LOG delete. When a profile is NOT in any lock state, right-clicking a row in any MACRO COST BASIS LOG popup now shows a DELETE button. Clicking DELETE pops a centered "CONFIRM TO DELETE THIS LOG" popup that shows the log's numeric value. Confirming removes the row from the log. The delete UI is hidden when a lock state is active. (9) Padlock-click hand-off. The black padlock above each PROFILE long name now correctly toggles LOCK PROFILE through the same code path as right-click LOCK PROFILE, so all four activation paths produce identical state and identical padlock UI updates everywhere. (10) VERSION STAMP. Bumped from C2.8 → C2.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.9', AND the JSON export __version__ field now reads 'C2.9'. The CSV download _<VERSION> suffix automatically advances to "_C2.9". The C2.8, C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.8May 2026Seven fixes on top of C2.7 plus the version bump. (1) CO. APPROVED RATES button styling. When CO. APPROVED RATES is active on P1/P2/P3, the active turquoise button now also gets a 2px BLACK OUTLINE and BLACK TEXT (not white). The has-data dot to the right of P1/P2/P3 turns into a BLACK padlock icon. Applied identically in the scroller AND in the ghost CUSTOMER CARD CLIPBOARD. (2) Has-data dot suppression. When SUBMITTED FOR REVIEW or CO. APPROVED RATES is active on a profile, the has-data dot is now REPLACED by the padlock icon instead of rendering both simultaneously. (3) Panel padlock for LOCK PROFILE. When a profile is locked via right-click → LOCK PROFILE, the padlock indicator now reliably appears above the PROFILE 1/2/3 tab in the panel AND in the CUSTOMER CARD CLIPBOARD ghost slot — identical to how the scroller already shows it. (4) Drag-drop lock protection. Profiles in CO. APPROVED RATES, SUBMITTED FOR REVIEW, or LOCK PROFILE state can no longer have data dropped INTO them. The same behavior is applied across all three lock variants and across scroller / clipboard / panel drop targets. (5) UNLOCK PROFILE popup everywhere. Any edit attempt against a profile in any of the three lock states now consistently pops a centered "UNLOCK PROFILE <name>" alert. LANDED $/PC inputs (per MACRO COST BASIS) are now also gated by the same profile-lock check. (6) CLIPBOARD swap confirm. Dragging a profile onto a green CLIPBOARD slot that already has data now pops a centered "CONFIRM CLIPBOARD DATA SWAP" confirm dialog before overwriting. Clicking CANCEL leaves the existing slot data untouched. (7) VERSION STAMP. Bumped from C2.7 → C2.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.8', AND the JSON export __version__ field now reads 'C2.8'. The CSV download _<VERSION> suffix automatically advances to "_C2.8". The C2.7, C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.7May 2026Five fixes on top of C2.6 plus the version bump. (1) P1 / P2 / P3 sub-profile buttons render in default gray when inactive. Only the active button shows its assigned color (P1 blue, P2 red, P3 purple) with bold white text. Switching active profiles swaps which one is highlighted. (2) Single white padlock (instead of two gold-silver emoji padlocks) appears on the active sub-profile button when CO. APPROVED RATES or SUBMITTED FOR REVIEW is active. The icon is a white inline SVG so it reads cleanly on both the black review background and the turquoise approved background. (3) Notes button now tracks the currently-active sub-profile. Switching from P1 to P2 (or P3) makes the Notes button expand the textarea for that profile, with the placeholder + title reflecting "Notes for PROFILE 2" / "3". (4) cardMode (CO. APPROVED RATES + SUBMITTED FOR REVIEW) is now per-sub-profile. The right-click menu offers these two options for P1, P2 AND P3 — not just MAIN/P1. roiGetCardMode + roiSetCardMode accept an optional subId arg (defaulting to roiActiveSub for backward compat). Everything that styles the cardMode (scroller button color, badge in panel tab row, padlock count) now reads the cardMode for the specific sub-profile being displayed. (5) Right-click on any P-button OR any panel PROFILE tab shows the same menu with all three options (LOCK / SUBMITTED FOR REVIEW / CO. APPROVED RATES). (6) VERSION STAMP. Bumped from C2.6 → C2.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.7', AND the JSON export __version__ field now reads 'C2.7'. The CSV download _<VERSION> suffix automatically advances to "_C2.7". The C2.6, C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.6May 2026Major unify on top of C2.5 plus the version bump. (1) Customer-card sub-profile buttons renamed: MAIN → P1, A → P2, B → P3. Each is now THE SAME thing as PROFILE 1 / PROFILE 2 / PROFILE 3 in the pack panels — two access points for the same data. (2) Pack-panel tab bar now always shows three tabs (PROFILE 1 / PROFILE 2 / PROFILE 3) bound directly to the customer-card sub-profiles. Clicking a tab swaps roiActiveSub and re-renders every pack panel. 10 Pack and 10 Pack MC now always show the same active profile (10 Pack MC follows 10 Pack via roiActiveSub). Same for 40 Pack ↔ 40 Pack MC. (3) Equal-width customer-card sub-profile buttons. Since every label is now the same 2-char length, the 3 buttons split the available width evenly. (4) Default colors per profile: P1 / PROFILE 1 stays blue (#0071e3) with bold white text. P2 / PROFILE 2 takes red (#ff3b30) — same as the TariffNews ticker label background. P3 / PROFILE 3 takes purple (#a352c5) — same as the old purple swatch in the right-click color bar. (5) Right-click menu loses the horizontal color bar. No more per-profile color picker — the default P1/P2/P3 colors above always apply. (6) Panel tab bar loses the + section button. There is no longer a notion of multiple "sections" inside one profile — each customer-card profile (P1/P2/P3) is its own profile with a Sell Price row + up to 3 Buyer rows. (7) Gray padlock above each PROFILE tab in the panel. Click it to toggle that profile's lock state. When locked it turns blue with an "L" suffix; the customer-card scroller and ghost-clipboard slots reflect the same lock state with their own padlock icon next to the P-letter. (8) ONE-TIME DATA MIGRATION on first C2.6 load: any data previously stored under sub-profile A (subId=2) or sub-profile B (subId=3) is permanently erased so P2 and P3 start fresh. Within MAIN (now P1), any older section 2 / section 3 data (s2_*, s3_*) is erased too — only s1_* (P1's rows) survive. The migration is gated by a localStorage flag so it never runs twice. (9) Default starting profile on app reload is P1 (subId=1); within a session your last-active profile per Customer Card persists. (10) VERSION STAMP. Bumped from C2.5 → C2.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.6', AND the JSON export __version__ field now reads 'C2.6'. The CSV download _<VERSION> suffix automatically advances to "_C2.6". The C2.5, C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.5May 2026Several fixes on top of C2.4 plus the version bump. (1) Scroller MAIN button now respects cardMode. The left-to-right customer-card scroller's MAIN sub-profile button now flips to black (SUBMITTED FOR REVIEW) or turquoise (CO. APPROVED RATES) with bold text and two gold/silver 🔒🔒 padlock emojis on the right side of "MAIN". Matches the styling already on the ghost CUSTOMER CARD CLIPBOARD slot. The plain LOCK PROFILE MAIN padlock keeps its existing white inline SVG so the two lock types are visually distinct. (2) CO. APPROVED RATES uses BLACK text (not white). The right-click menu option button, the MAIN scroller button, the ghost-slot MAIN button, and the badge in the +PROFILE row all render with black bold text on the turquoise background. The badge also picks up a black outline; the right-click menu button gets the same outline. SUBMITTED FOR REVIEW keeps its white-on-black styling. (3) Right-click menu gray-out. When one mode is active (review OR approved), every other interactive option in the menu (the OTHER mode option, RESET COLOR, CLEAR PROFILE MAIN, LOCK PROFILE MAIN, and the horizontal color bar) renders disabled and ignores clicks. Only the active mode button stays clickable so the user can deselect it and recover the rest of the menu. (4) No new profiles when locked. Clicking the "+" on the profile tab row is now gated by a profile-lock check. If the active sub-profile is LOCKED (via LOCK PROFILE MAIN / A / B) OR the active sub-profile is MAIN and the customer card is in SUBMITTED FOR REVIEW or CO. APPROVED RATES mode, the + click pops a center-screen "UNLOCK PROFILE <name>" alert and refuses to add a profile. (5) RESET button popup tells you which profile to unlock. When you click RESET on a pack panel whose active sub-profile is locked, the popup now reads "UNLOCK PROFILE MAIN" / "A" / "B" instead of the old generic text. The HIDE button is intentionally NOT gated — you can still toggle the screen-shot-friendly view while a profile is locked. (6) Notes are read-only when locked. While the active sub-profile is locked (any lock variant), the per-pack notepad and the per-profile NOTES textarea both fall back to read-only. Click the NOTES button to open / close its panel still works so you can READ the saved notes. Trying to edit or delete inside a locked profile pops "UNLOCK CUSTOMER CARD PROFILE TO MAKE CHANGES.". Works the same in Globe-on AND Globe-off note modes. (7) Every price input, promo input, custom heading input, and click-to-edit margin pct / dollar in the 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panels is now gated by the same profile-lock check. HIDE is the only edit-zone button that always works while locked. (8) VERSION STAMP. Bumped from C2.4 → C2.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.5', AND the JSON export __version__ field now reads 'C2.5'. The CSV download _<VERSION> suffix automatically advances to "_C2.5". The C2.4, C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.4May 2026Three changes on top of C2.3 plus the version bump. (1) MAIN profile right-click menu adds two top options ABOVE the color palette: "CO. APPROVED RATES" (top) and "SUBMITTED FOR REVIEW" (just below it). Each is a toggle. Activating "SUBMITTED FOR REVIEW" turns the MAIN profile button black with white bold font, adds a second padlock to the right of the existing padlock, and pins a black "LOCKED - PENDING REVIEW" badge to the LEFT of the Notes button on the +PROFILE row of every pack panel. Activating "CO. APPROVED RATES" turns the MAIN profile button turquoise (#40E0D0) with white bold font, adds a second padlock the same way, and pins a turquoise badge to the LEFT of the Notes button. When either state is active, the matching menu row renders as a colored button (black or turquoise) with white bold font and reads either "UNLOCK FROM REVIEW" or "CO. APPROVED RATES". Clicking either while active pops a center-screen confirm ("CONFIRM TO UN-SUBMIT FOR REVIEW" / "CONFIRM TO DESELECT - CO. APPROVED RATES") before reverting. Modes are mutually exclusive — activating one auto-clears the other. State persists per (Customer Card × MAIN sub-profile) via roiCardData[cardId]['1'].cardMode = 'normal' / 'review' / 'approved'. (2) Profile CLIPBOARD slots (the two original profile clipboards, NOT the customer-card clipboards) now auto-mirror their source profile in real time. Every time the source (card + sub) gets a state/promos update via the normal save path, the clipboard re-reads and re-renders so its visible value-summary always matches what is in the source. Catches the case where you load a clipboard later and would otherwise drag stale values into another profile. (3) VERSION STAMP. Bumped from C2.3 → C2.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.4', AND the JSON export __version__ field now reads 'C2.4'. The CSV download _<VERSION> suffix automatically advances to "_C2.4". The C2.3, C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.3May 2026One change on top of C2.2 plus the version bump. (1) Per-profile Notes is now a small button on the far right of the PROFILE tab row instead of a centered hyperlink below it. It sits on the same horizontal line as "+ PROFILE 1 (- when more than one)", pushed to the right via margin-left:auto. Click toggles the expandable textarea below the tab row exactly like before; the textarea autosaves into roiCardData[card][sub].profileNotes[mpn][N] per usual. (2) VERSION STAMP. Bumped from C2.2 → C2.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.3', AND the JSON export __version__ field now reads 'C2.3'. The CSV download _<VERSION> suffix automatically advances to "_C2.3". The C2.2, C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.2May 2026Two fixes on top of C2.1 plus the version bump. (1) Panels were rendering blank below the LANDED readout (no PROFILE buttons, no MFR Sell Price section, nothing). Root cause: the C2.1 per-profile Notes helpers (_isProfileNotesOpen, _readProfileNotes, etc.) were injected into the trailing C1.4 IIFE, but _buildProfileTabsHtml (which calls them) lives inside the earlier C0.4 IIFE. Because that IIFE runs "use strict", the missing identifier threw a ReferenceError on every render and the inner-panel paint aborted. C2.2 relocates the helpers into the C0.4 IIFE next to _mcSkuIndex so the renderer can see them again. The trailing duplicates are removed. (2) Tidied a stray duplicate window.roiHideToggle assignment left over by the C2.1 HIDE-button revert. Cosmetic. (3) VERSION STAMP. Bumped from C2.1 → C2.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.2', AND the JSON export __version__ field now reads 'C2.2'. The CSV download _<VERSION> suffix automatically advances to "_C2.2". The C2.1, C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.1May 2026Three changes on top of C2.0 plus the version bump. (1) REVERT C2.0 PROMO behavior. The collapsible "ADD PROMO" link and the promo cascade are gone — promos are once again rendered ONLY on the final sell slot per section (sell row when no buyer rows exist; otherwise on the last buyer row) and they no longer feed into the next slot's base price. Matches C1.9 behavior. (2) REVERT HIDE button 2-option menu. Clicking HIDE is back to a single-click toggle that hides LANDED + PRODUCER MARGIN and any empty ADD-PROMO blocks (the C1.5 / C1.9 behavior). Another click flips back to Show. The MC sibling still cascades. (3) Per-profile NOTES link. A small blue underlined "Notes" hyperlink sits centered under the PROFILE tab bar (to the right of the + button). Click it and a compact textarea expands below the LANDED row where you can leave notes for the ACTIVE profile only. Each profile keeps its own notes (so PROFILE 1, PROFILE 2, and PROFILE 3 each have their own independent notepad). Notes autosave on every keystroke into roiCardData[card][sub].profileNotes[mpn][N], so they persist across reload + Customer Card / Sub Profile / Basis / Pack switches. Globe deactivation confirm popup from C2.0 stays. (4) VERSION STAMP. Bumped from C2.0 → C2.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.1', AND the JSON export __version__ field now reads 'C2.1'. The CSV download _<VERSION> suffix automatically advances to "_C2.1". The C2.0, C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C2.0May 2026Four changes on top of C1.9 plus the version bump. (1) New PROMO UX. Every slot in every section (MFR Sell + every Buyer row) now shows a small blue underlined "ADD PROMO" link under its main value input. Click the link to expand a compact promo % input + MARGIN AFTER PROMO readout. Once a promo > 0 is entered the block stays expanded; setting it back to 0 leaves the input visible until next render but the readout hides. (2) Promos now CASCADE through the slot chain inside a section. A promo on the MFR Sell row computes a Final $, and that Final $ becomes the BASE PRICE for the BUYER 1 margin. If Buyer 1 also has a promo, its Final $ becomes the base for Buyer 2. Same pattern continues through Buyer 3. Both the on-screen ROI-Calc and the ROI PDF now use this cascade. (3) HIDE button pops a 2-option menu above the button. "HIDE with Promos" works exactly like the old HIDE (empty promo blocks vanish, LANDED + PRODUCER MARGIN hide). "Hide keep Promos" hides LANDED + PRODUCER MARGIN but leaves every promo block visible (filled OR empty). One click on the active HIDE button cycles back to the SHOW state and re-shows everything. (4) Globe deactivation now pops a center-screen confirm: "REMOVE ALL NOTES FROM ALL MACRO COST BASES?". Clicking OK (YES) wipes every Macro Cost Basis's note for that panel and clears the visible textarea. Clicking Cancel (NO) leaves the existing notes intact and just breaks the sync connection so future edits scope to the current basis only. Activating the Globe still pops its existing confirm before the overwrite-to-all-bases. (5) VERSION STAMP. Bumped from C1.9 → C2.0 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C2.0', AND the JSON export __version__ field now reads 'C2.0'. The CSV download _<VERSION> suffix automatically advances to "_C2.0". The C1.9, C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.9May 2026Two changes on top of C1.8 plus the version bump. (1) Notes "Globe" sync toggle. A small globe button sits to the LEFT of each Notes area in every pack panel (10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC). When the globe is INACTIVE (default), notes stay scoped per Macro Cost Basis as they always have. When you ACTIVATE the globe, the current note for that panel is copied across every Macro Cost Basis (PALLET / TEU / FEU + any custom bases) AND every future keystroke continues to mirror to all bases, so the notes you see for that panel are the same regardless of which basis is selected. Activating pops a confirmation dialog warning that whatever notes existed in other bases will be overwritten. Click the globe again to deactivate — existing copies stay where they are, but future edits only update the current basis. State persists per Customer Card × Sub Profile × panel via roiCardData[card][sub].notesGlobe[mpn]. (2) Pre-Paid Ship-Calc math review. The data sources + computeRow formulas + four-scale rollups (TOTAL / 1 MC / 1 PACK / 1 BLADE) were re-walked against the new section + buyer model. The chain is: Ship-Calc reads MC-level sell prices from roiState[basis][MC_SKU.mpn].s${N}_sell, reads sectionsN / s${N}_buyers from the matching base SKU's record (since structure is shared across all 4 SKU mpns since C1.6), applies the LAST-slot promo (sell row when buyersN === 0; if buyersN > 0 the visible sell row has no promo because the promo lives on the last buyer which isn't shown in Ship-Calc since C0.9), and computes margin per scale by scaling the MC price by revMul (1 / packsPerMC / packsPerMC×pack) and landed by the same factor. If you're seeing wrong values, please share a specific repro (which panel, which scale card, which row, what value shows vs what you expect) so I can pin down which step in the chain is off — my pass didn't turn up a math bug. Common pitfall: prices on the 10 Pack panel are SEPARATE from prices on the 10 Pack MC panel, and Ship-Calc only reads MC-level prices — so if you entered values only on the 10 Pack panel, the 10 Pack MC sell field is empty and Ship-Calc renders zeros / no-price markers. (3) VERSION STAMP. Bumped from C1.8 → C1.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.9', AND the JSON export __version__ field now reads 'C1.9'. The CSV download _<VERSION> suffix automatically advances to "_C1.9". The C1.8, C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.8May 2026Four fixes on top of C1.7 plus the version bump. (1) LANDED row left-aligned with the "+" button. The inline indent that used to push it past the +/PROFILE label is gone, so the L in LANDED now sits at the same x-coordinate as the "+". (2) Notes cap back to 108 (54 chars on the top row + 54 on the bottom). (3) Profiles are now horizontal TABS instead of stacked sections. A single profile still reads "+ PROFILE 1" in plain text. Clicking + adds a PROFILE 2 tab to the right, then PROFILE 3 the same way (max 3). When there are 2 or more profiles each one becomes a clickable button; the active tab is highlighted and only that profile's rows render in the panel below. The "." after each PROFILE number is removed everywhere. A "−" button appears next to the active tab when there are 2+ profiles so you can collapse / erase the current one. Adding a profile auto-switches the active tab to the new one. Each pack panel (10 Pack, 10 Pack MC, 40 Pack, 40 Pack MC) keeps its own independent active tab so you can view PROFILE 1 on 10 Pack while looking at PROFILE 2 on 40 Pack MC. (4) Click-to-edit margin $ now updates the matching margin % in real time on every keystroke (and vice-versa). Previously editing the $ amount paused the % readout because _paintMargin skipped repainting whenever either cell had its editing flag set; the skip is now per-cell — only the cell you are typing in is left alone, the other cell updates live. (5) VERSION STAMP. Bumped from C1.7 → C1.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.8', AND the JSON export __version__ field now reads 'C1.8'. The CSV download _<VERSION> suffix automatically advances to "_C1.8". The C1.7, C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.7May 2026Six fixes on top of C1.6 plus the version bump. (1) LANDED line moves to its own row directly below the PROFILE row. Same vertical breathing room as the rows that follow it (PROFILE 1. / LANDED / MFR Sell Price). Aligns cleanly across all four pack panels. (2) Notes hard cap raised to 114 characters total (54 on the top row + 60 on the bottom). Still blocks at the cap so prior text is never erased. (3) HIDE no longer shifts the page. Hidden cells now use visibility:hidden instead of display:none, so the panel keeps its full height and the cursor stays exactly where you clicked. Extra gray browser background appears below instead of the rest of the panel jumping up under your mouse. (4) PRODUCER MARGIN $ value is now click-to-edit too. Single-click the gray $ amount, type a new dollar figure (or leave it negative for an underwater margin), press Enter or click out — the matching Sell / Buyer price input back-solves from the new $ delta. BUYER N MARGIN dollar values get the same treatment. Live recompute on every keystroke. (5) Heading rename — "MSRP / Buyer N Sell Price" becomes "Buyer N Sell Price / MSRP" (and the corresponding margin head changes from "MSRP / BUYER N MARGIN" to "BUYER N MARGIN / MSRP") so the MSRP tag sits at the end of the line instead of leading it. Applied to the renderer, the Pre-Paid Ship-Calc row labels, and the PDF exports. The cascade still works: an upstream buyer with a downstream sibling reads as "Buyer N Sell Price (Buyer N+1 Cost)" (unchanged). (6) Notes autosave verified — every keystroke calls roiNoteSave which writes the text into roiCardData[card][sub].notes[basis][mpn] and immediately calls roiCardDataSave to persist roiCardData to localStorage. On reload, roiCardDataLoad rehydrates the tree and roiHydrateNotes pushes the right note into each visible panel when you swap Customer Card / Sub Profile / Basis / Pack. Path is unchanged from prior versions. (7) VERSION STAMP. Bumped from C1.6 → C1.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.7', AND the JSON export __version__ field now reads 'C1.7'. The CSV download _<VERSION> suffix automatically advances to "_C1.7". The C1.6, C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.6May 2026Three fixes on top of C1.5 plus the version bump. (1) Right-click margin menu — the top option label is renamed from "Do for All Sub-Sections" to "Pull All Margins from 10 Pack Panel" (or "... from 40 Pack Panel" on a 40 Pack MC margin). Reads more like what the option actually does. (2) Notes overflow — switched from visual-measurement to a hard 108-character cap (54 chars per row × 2 rows). Once you hit 108 characters, the next key just doesn't register. Paste is trimmed to 108 chars. Backspace / Delete / arrow navigation still work normally. No more erased-tail behavior. (3) Section + buyer add/remove now syncs across all four pack panels for the same PROFILE. Adding Buyer 2 on 10 Pack adds it on 10 Pack MC, 40 Pack, and 40 Pack MC too. Removing a section / buyer cleans up the matching field + promo data on every SKU's record. Each panel retains its own independent prices, but the structural shape (how many sections + how many buyers per section) is unified. (4) VERSION STAMP. Bumped from C1.5 → C1.6 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.6', AND the JSON export __version__ field now reads 'C1.6'. The CSV download _<VERSION> suffix automatically advances to "_C1.6". The C1.5, C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.5May 2026Five fixes on top of C1.4 plus the version bump. (1) Margin pct editing — only 1 character could be typed, and characters could appear to type backwards. Root cause: the live-margin handler called _roiRecalcSku which then called _paintMargin to repaint every margin pct including the one being edited, overwriting your text and resetting the caret to start. _paintMargin now skips the element whose data-editing flag is set, so the in-progress text + caret position survive every keystroke. (2) "Pull Margin % from 10 Pack Panel" and "Do for All Sub-Sections" buttons did nothing. Root cause: _curSku / _baseSkuIndex / _mcSkuIndex are declared inside the C0.4 IIFE so the trailing C1.4 IIFE could not see them — every call threw a ReferenceError that the try/catch silently swallowed. Helpers are now redefined inline at the top of the C1.4 IIFE. (3) HIDE button in a base panel now also toggles the matching MC panel’s hide state (10 Pack ↔ 10 Pack MC, 40 Pack ↔ 40 Pack MC). One click hides PRODUCER / LANDED data across both panels simultaneously. (4) Notes overflow: typing a character past the 2-row cap used to trigger the chunked-trim that erased the last 5-10 characters. The chunked-trim path inside roiNoteCapToMax is replaced with a no-op, and a new keydown blocker on the notes area prevents new character insertion once the area is full (while still allowing Backspace, Delete, arrows, etc). You can now type to the end and the next key just won’t register, instead of erasing what you already had. (5) VERSION STAMP. Bumped from C1.4 → C1.5 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.5', AND the JSON export __version__ field now reads 'C1.5'. The CSV download _<VERSION> suffix automatically advances to "_C1.5". The C1.4, C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.4May 2026Six fixes on top of C1.3 plus the version bump. (1) Per-section LANDED value moves inline next to its heading (no more right-aligned-to-the-edge layout) and is recolored medium gray (#6b7280) to match the rest of the secondary value type. So "PROFILE 1. LANDED / 10 PACK $3.94" reads as one continuous heading line. (2) Live margin → price recompute. Editing a green PRODUCER MARGIN or BUYER N MARGIN percent now updates the matching Sell / Buyer price input on every keystroke, no longer waiting for Enter or blur. Press Enter or click out to finalize the value into storage; press Escape to revert. (3) Right-click any margin pct on a 40 Pack / 10 Pack MC / 40 Pack MC panel and a small popup offers two options. The TOP option is "Do for All Sub-Sections" — pulls every margin in the section's current PROFILE from the source panel and back-solves every matching price. The SECOND option is per-row: "Enter Corresponding % from 10 Pk Panel" on a 40 Pack panel, "Pull Margin % from 10 Pack Panel" on a 10 Pack MC panel, or "Pull Margin % from 40 Pack Panel" on a 40 Pack MC panel. Source panel mapping: 40 Pack base ← 10 Pack base, 10 Pack MC ← 10 Pack base, 40 Pack MC ← 40 Pack base. PROFILE-scoped (i.e. PROFILE 1 pulls from PROFILE 1 only). 10 Pack base panel has no right-click menu since there's no upstream source to pull from. (4) HIDE button is now centered under RESET on every pack panel — both buttons anchor to right:0 with matching padding so they form a vertical pair instead of drifting. (5) Notes panel overflow no longer erases trailing characters. The contenteditable note areas now use a beforeinput listener that checks whether a new insert would overflow the 2-row visual cap, and if so, prevents the insert outright. Cursor stays at the end, you stop being allowed to type more, and the last 5-10 chars you already typed remain intact. Paste path still trims to fit because a paste can be any length. (6) VERSION STAMP. Bumped from C1.3 → C1.4 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.4', AND the JSON export __version__ field now reads 'C1.4'. The CSV download _<VERSION> suffix automatically advances to "_C1.4". The C1.3, C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.3May 2026Several changes on top of C1.2 plus the version bump. (1) ROI section heading renamed from "N." (e.g. "1. LANDED / 10 PACK") to "PROFILE N." (e.g. "PROFILE 1. LANDED / 10 PACK"). Applied to every section heading in 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panels. PDF and Ship-Calc displays follow. (2) Per-panel HIDE button. A HIDE / SHOW toggle sits beside the RESET button on every pack panel. Clicking HIDE blanks every LANDED readout (Piece / Pack / Pk MC / Pallet) AND every PRODUCER MARGIN row across all sections in that panel, plus collapses any ADD-PROMO block that has no value entered. Clicking it again restores. Lets you screen-shot a panel for a Buyer without leaking landed cost or producer margin. (3) FAVORITES panel z-index fix on chart preview. The FAVORITES popup inside chart preview windows was rendering behind the chart itself. z-index raised so it always sits on top of the chart and stays open until the FAVORITES button is clicked again or focus moves outside the popup. (4) FAVORITES button on the MACRO COST BASIS "LOGGED VALUES" chart, between the "N-PACK SUBTAB" heading and the X close button. Light yellow by default, bright yellow + black text on hover and while active. Click toggles a FAVORITES sub-panel that lists only the logged values whose star is set. (5) DELETE + favorite star per LOGGED VALUE row. Each entry in the LOGGED VALUES chart now carries a faded star (left) and a DELETE button (right). Click the star to promote a value to FAVORITES (star fills bright yellow); click again to demote. DELETE pops a centered "CONFIRM TO DELETE VALUE $X.XX" modal. Favorites view hides the DELETE buttons — values can only be removed from favorites by clicking the yellow star (demote-to-list). (6) LAST LOADED tag on the previously-loaded chart. After you load a different chart, the chart that WAS active before the swap shows a "LAST / LOADED" stacked tag to the left of its heading + date/time stamp in the CHART HISTORY panel. Helps you visually backtrack to where you came from. (7) Tariff History PDF buttons split apart. The top-right "PDF" button on a tariff history entry exports EVERY block (every dated heading + all underlying data) inside that history record. The per-section "Download PDF" button exports only the block it sits on, matching the user-facing distinction between the two affordances. (8) Click-to-edit margin percentages. The green margin percent value on any sub-section row (PRODUCER MARGIN / BUYER N MARGIN / MARGIN AFTER PROMO) is now click-to-edit. Single-click turns it into an inline number you can type. Press Enter or click out to commit. The matching price input above is back-solved from the new margin (price = base / (1 - margin/100)) so typing 30% under PRODUCER MARGIN populates the Sell Price input with whatever number yields a 30% margin on the current landed cost. No extra big entry box renders — just the same compact numbers, now editable. (9) PDF spacing: additional vertical-overlap cleanup in spots that still rendered tight in C1.2. (10) VERSION STAMP. Bumped from C1.2 → C1.3 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.3', AND the JSON export __version__ field now reads 'C1.3'. The CSV download _<VERSION> suffix automatically advances to "_C1.3". The C1.2, C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.2May 2026Three fixes on top of C1.1 plus the version bump. (1) PDF: overlapping row text fixed. Row leading values were ~2 pt too tight for the font sizes in use, so 10-pt body lines (Buyer 1 Sell Price ...) were bleeding into the next 9-pt sub-line (BUYER 1 MARGIN ...) and the following 10-pt label. Each y-increment in the per-SKU section loop is now sized to the font baseline + 50% leading: 11-pt section heads advance 16 pt, 10-pt row labels advance 15 pt, 9-pt margin / promo / MC-ref sub-lines advance 13 pt. Ensure() calls adjusted to match the new minimums. (2) MARGIN AFTER PROMO — new "Final" readout. Both the on-screen ROI-Calc and the PDF now print the effective sell price after promo (slot price × (1 − promo%/100)) on the same line as the margin readout. On screen, it sits to the right of the "$X.XX | XX.XX%" numbers in a blue hyperlink-style color (#2563eb) reading "Final $X.XX". In the PDF, the same blue Final value is right-aligned on the AFTER <N>% PROMO line so it pops out at first glance. (3) DIST PDF sell-row promo formatting: the "eff. price $X.XX" phrasing is replaced by the same right-aligned blue "Final $X.XX" readout so the visual pattern matches between FULL and DIST. (4) VERSION STAMP. Bumped from C1.1 → C1.2 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.2', AND the JSON export __version__ field now reads 'C1.2'. The CSV download _<VERSION> suffix automatically advances to "_C1.2". The C1.1, C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.1May 2026Four fixes on top of C1.0 plus the version bump. (1) PDF: promos always export. Any time a slot has a promo % set in storage (sell-row promo when sell is the section's final slot, OR a buyer-row promo on the section's final buyer), an "AFTER <N>% PROMO:" line now renders alongside the standard MARGIN line. In FULL mode the sell-row promo prints under PRODUCER MARGIN; in DIST mode the sell-row promo is omitted (since the producer margin itself is hidden in DIST) but the BUYER-row promo prints in both variants because that's a public-side margin. Stale promo data on a non-final slot is intentionally skipped to match the on-screen ROI-Calc behavior (promo only renders on the most-downstream sell price per section). (2) PDF: vertical spacing tightened. The big-orange "10 PACK ROI" bar → SKU/MPN title → thin black rule → LANDED COST READOUT chain now reads as one visual unit: gaps tightened from ~10 pt to ~4 pt so the reader scans a section as a single block. Inter-section gaps (the light-gray divider between SECTION 1 and SECTION 2 inside one SKU panel) are also tightened. The y += 6 / ensure(34) chunk that opened each SKU now reads y += 4 with no double-leading-whitespace inside the title block. (3) PDF: SKU/MPN heading color medium gray. Every "10 PACK — SKU/MPN 10010 · UPC 785366284171" subheading line is now printed in medium gray (RGB 100,100,100) instead of near-black so it reads as a section header rather than competing with the data rows. (4) Cascaded buyer labels. In both the on-screen ROI-Calc panel renderer AND the ROI PDF, an upstream buyer row now reads "Buyer N Sell Price (Buyer N+1 Cost)" once a downstream buyer exists. So adding Buyer 2 renames Buyer 1 from "Buyer 1 Sell Price" to "Buyer 1 Sell Price (Buyer 2 Cost)", and adding Buyer 3 renames Buyer 2 to "Buyer 2 Sell Price (Buyer 3 Cost)". The final / sole buyer keeps the "MSRP / Buyer N Sell Price" label since it stays the actual MSRP. (5) VERSION STAMP. Bumped from C1.0 → C1.1 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.1', AND the JSON export __version__ field now reads 'C1.1'. The CSV download _<VERSION> suffix automatically advances to "_C1.1". The C1.0, C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C1.0May 2026Eight fixes on top of C0.9 plus the version bump. (1) CUSTOMER CARDS popup click no longer auto-scrolls the page. Selecting a different card in the popup still horizontally scrolls the left-to-right CUSTOMER CARDS strip so the activated card sits in view, but the page vertical position is preserved — you can stay scrolled down on the margin panels and the newly-activated card is just there waiting if you scroll back up. (2) MFR-Cost Calc button renames: "Blade Component Entry" → "Part Component Entry", "Completed Blade Build Entry" → "Completed Product Entry", "Bulk Cost Build Entry" → "Bulk Cost Entry". Functionality unchanged. (3) ROI-Calc "Sell Price (Direct to Retail)" renamed to "MFR Sell Price (Direct to Retail)" and "Sell Price (Buyer 1 Cost)" renamed to "MFR Sell Price (Buyer 1 Cost)" in every section heading (10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC) and in the Pre-Paid Ship-Calc row labels. (4) Completed Product Entry (formerly Completed Blade Build) "Total Blade Cost per PC" input was disabled after switching back from the Bulk Cost mode. Root cause: the bulk mode disabled mTotalBladeCost via _setBulkDim, but the non-bulk branch in mfrSetEntryMode never set disabled = false. Fixed: the non-bulk path now resets disabled on mTotalBladeCost, mStampedCost, and mPvcCost before applying the mode-specific readOnly + disabled state. (5) 3rd CUSTOMER CARD CLIPBOARD ghost slot right-click menu now shows REMOVE FROM CLIPBOARD at the top, matching the other two slots. The data-clipboard-slot detection was gated on v === 0 || v === 1 (C0.9 had only 2 slots in mind); extended to also accept v === 2. (6) Pre-Paid Ship-Calc "SECTION 1" / "SECTION 2" / etc. heading color is darker so the labels stand out from the row content. (7) ROI PDF download rewrite. The downloadable "roi-<card>-profile<n>-<basis>-<ver>.pdf" (both per-basis and combined ALL-COST-BASIS variants) now reads the new section + buyer storage model, the same way the on-screen ROI-Calc panels do. The FULL variant prints each section's LANDED line, Sell Price + heading + PRODUCER MARGIN, every visible Buyer 1-3 row with its heading + BUYER N MARGIN, MC reference values under MC inputs, and any ADD PROMO / MARGIN AFTER PROMO that applies to the section's final sell slot. The DIST variant omits every LANDED line and every PRODUCER MARGIN row, showing only the Sell Price + Buyer 1-3 prices and their buyer-to-buyer margins so the recipient can't reverse-engineer the producer's landed cost. The spurious "last logged: $[object Object]" / "$[object Object],[object Object]" footer strings are gone — the basis-log array is now read correctly (it stores {value, ts} entries since C0.2; pre-C0.2 string entries are still picked up too). (8) VERSION STAMP. Bumped from C0.9 → C1.0 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C1.0', AND the JSON export __version__ field now reads 'C1.0'. The CSV download _<VERSION> suffix automatically advances to "_C1.0". The C0.9, C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.9May 2026Three fixes on top of C0.8 plus the version bump. (1) MFR-Cost Calc "Total Bulk Cost" button renamed to "Bulk Cost Build Entry" so all three mode buttons read in the same Verb + Cost + Entry format. Functionality unchanged. (2) Custom-heading input height matches the readonly GRAINGER badge. The editable heading inputs (with padlock + the "(CUSTOM HEADING)" placeholder ones) were rendering noticeably taller than the read-only Sell-Price card-name badge above them. Both now use display:inline-flex with a fixed 20-px height, 0 vertical padding, and identical font metrics so they sit at the exact same height on every section heading row. The padlock still indicates editability when a heading is set; empty inputs still show the dashed "(CUSTOM HEADING)" placeholder. (3) Pre-Paid Ship-Calc renders only Item #1 per section. The four scale cards (TOTAL / 1 MC / 1 PACK / 1 BLADE) now show just the Sell Price row inside each SECTION block — the MSRP / Buyer 1-3 rows are no longer iterated. The light-gray section divider sits between sections as before. Sell-row promo logic is preserved: when a section has no buyer rows, the Sell row IS the final slot and any promo entered on it is applied (effRaw = sell × (1 - promo/100)) before subtracting landed + freight, so the discount reduces the PRODUCER ADJUSTED MARGIN as expected. When a section has at least one buyer row, the promo lives on the last buyer (not visible in the Ship-Calc per this change) and the Sell row shows its un-discounted margin minus freight only. (4) VERSION STAMP. Bumped from C0.8 → C0.9 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C0.9', AND the JSON export __version__ field now reads 'C0.9'. The CSV download _<VERSION> suffix automatically advances to "_C0.9". The C0.8, C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.8May 2026Three fixes on top of C0.7 plus the version bump. (1) CUSTOMER CARDS popup window now actually shows cards. The C0.7 popup tried to read window.opener.roiCustomerCards directly, but the variable is declared with let (not var), so it's not a property of window — the popup got an empty list. The opener now exposes a window.gbPopupGetCardData() helper that returns {cards, activeCard}, and the popup calls that on every 500 ms tick. Selecting a card still calls gbCustCardsDdSelect() on the opener, which is a top-level function declaration and has always been accessible. (2) Pre-Paid Ship-Calc data is back. C0.7 introduced a temporal-dead-zone bug: const sectionsN was declared BEFORE const baseHdrRec, but read baseHdrRec._sections in its initializer. That threw a ReferenceError mid-render, aborted shipCalcRefresh(), and left the "Pick a Customer Card…" placeholder onscreen even when every input was filled. Fixed by declaring baseSkuForShip + baseHdrRec BEFORE sectionsN reads from them. (3) MFR-Cost Calc "Total Bulk Cost" mode. A third entry-mode button sits to the LEFT of "Completed Blade Build" and "Blade Component Entry". Selecting it grays out every existing input (Stamped, PVC, Total Blade Cost, MFR Pack Cost, MC Cardboard Cost) except the 10 Pack / 40 Pack selector, which stays active for reference. Two new inputs appear: "Total MFR Ship-Ready Cost" and "Total Ship-Ready Blade Count". Entering both auto-divides cost by count to produce a "Ship-Ready Cost Per Blade" line that appears in the existing results block (with the big orange PDF button, total heading, and itemized math) the same way the normal modes render. Switching back to Completed / Component preserves the bulk inputs so you can flip between modes without retyping. (4) VERSION STAMP. Bumped from C0.7 → C0.8 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C0.8', AND the JSON export __version__ field now reads 'C0.8'. The CSV download _<VERSION> suffix automatically advances to "_C0.8". The C0.7, C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.7May 2026Five fixes on top of C0.6 plus the version bump. (1) CUSTOMER CARDS popup window. Clicking the CUSTOMER CARDS button no longer opens an in-page dropdown panel — it opens a real browser popup window that can be dragged outside the main GB-BizCalc browser window and parked anywhere on the desktop (or on a second monitor). The popup is session-scoped: as long as the main window stays open, the popup stays where you left it and the same window is re-focused on every subsequent click. Closing and reopening the GB-BizCalc page opens it back in the default position beside the CUSTOMER CARDS button. If the browser blocks the popup, the legacy in-page dropdown opens as a fallback. The popup auto-refreshes from the main window every 500 ms so new cards / renames / active-card highlights stay in sync. (2) Edit Text preserves the heading. Picking "Edit Text" from the buyer-row padlock menu now leaves the existing heading text in the input so you can tweak or delete it — before, choosing Edit Text wiped the value first. Internally tracked via a transient unlocked-headings set, cleared on the input's blur. (3) Pre-Paid Ship-Calc shows every section. The Ship-Calc was reading the section count off the MC SKU's record, but section / buyer counts are stored on the BASE SKU (10 Pack / 40 Pack). It only rendered Section 1. Now reads the count off the matching base SKU's record, so Section 2 and 3 actually show up with their own freight-adjusted margin rows on every scale card (TOTAL / 1 MC / 1 PACK / 1 BLADE). (4) Ship-Calc row labels split with lighter-gray extras. The row label now renders as "Sell Price" (default weight + color) followed by "(Direct to Retail) <HEADING>" in lighter gray, so the price-row name reads clearly even with a long heading. The same split applies to buyer rows: "MSRP / Buyer 1 Sell Price" main + "<HEADING>" in lighter gray. (5) ADJUSTED MARGIN re-positioned + re-sized. The top label is now indented to start at the same horizontal position as the row text (the "A" aligns above the "S" in Sell Price). Font size restored to the C0.5 12-px size so it reads cleanly. The right-side column header now reads "PRODUCER ADJUSTED MARGIN" instead of "GRIPBLADES ADJUSTED MARGIN" (matches the rest of the new ROI-Calc terminology). (6) VERSION STAMP. Bumped from C0.6 → C0.7 throughout: title bar, version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C0.7', AND the JSON export __version__ field now reads 'C0.7'. The CSV download _<VERSION> suffix automatically advances to "_C0.7". The C0.6, C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.6May 2026Seven fixes on top of C0.5 plus the version bump. (1) Heading input box height + auto-width. The faded heading text box next to each section heading is now the same compact height as the readonly "Sell Price" badge that shows the active customer card's name. Its width auto-scales to fit the text in it so 22-character chain-store names no longer clip. (2) Section alignment across pair panels. When you add a new section to the 10 Pack or 40 Pack base panel, the matching section in the 10 Pack MC / 40 Pack MC panel is now padded so their section headings AND the thin black section dividers sit on the exact same horizontal line across both panels. If the base section has more rows than the MC section (or vice-versa), extra whitespace is added to the shorter side so the next section's "N. LANDED / ..." line still reads across the screen. Re-runs automatically after every Add Buyer / Remove Buyer / Add Section / Remove Section / input edit. (3) 40 Pack pack-size fixes. The renderer was using the 10 Pack SKU for every panel index, which made the 40 Pack panel show "LANDED / 10 PACK" instead of "LANDED / 40 PACK", and the MC reference values under the 40 Pack MC inputs were computing with the wrong multiplier (x120 instead of x24). Fixed: every section / heading / multiplier now reads off the current subtab's SKU. (4) Multiplier chain verified. The "LANDED / N Pk MC (x...)" cell and every "($base × mult = $total)" reference value under MC inputs both pull their multiplier from PACK_DATA / SHIPPING REFERENCE through ROI_PACKS_PER_MC. Editing "Packs Per Master Carton" or "Master Cartons Per Pallet" on the SHIPPING REFERENCE chart now reflects everywhere downstream (LANDED / N Pk MC, LANDED / PALLET, and the per-section MC reference values). (5) Buyer headings mirror to MC. Custom headings entered on the 10 Pack panel (or 40 Pack panel) now also appear on the matching row of the 10 Pack MC (or 40 Pack MC) panel as a read-only badge. Only the base panel has the editable heading input + padlock; the MC panel reflects whatever was last set on its base sibling. (6) Ship-Calc row labels include the heading. Each row in every scale card (TOTAL / 1 MC / 1 PACK / 1 BLADE) now reads "1. Sell Price (Buyer 1 Cost) <HEADING>" or "2. MSRP / Buyer 1 Sell Price <HEADING>" etc. A small "ADJUSTED MARGIN" line sits above the row label to make it clear that the dollar/percent on the right is the freight-adjusted margin. Empty / unset headings just omit the heading suffix. (7) No auto-scroll on ROI subtab switch. Clicking 10 PACK ROI or 40 PACK ROI no longer jumps the viewport up to the CUSTOMER CARDS bar. The page stays where you were. Selecting a card from the scroller or dropdown still scroll-into-views the card itself (so the active card is always visible in the scroller), and ghost / CUSTOMER CARD CLIPBOARD clicks continue to be inert per C0.4 #1. (8) VERSION STAMP. Bumped from C0.5 → C0.6 throughout: title bar (GB-BizCalc - C0.6), version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C0.6', AND the JSON export __version__ field now reads 'C0.6'. The CSV download _<VERSION> suffix automatically advances to "_C0.6". The C0.5, C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.5May 2026Seven follow-up changes on top of C0.4 plus the version bump. (1) ADD PROMO compact layout. The ADD PROMO heading + % input + MARGIN AFTER PROMO readout now sits in a single horizontal row matching the Sell Price → PRODUCER MARGIN layout instead of stacking vertically. Each section uses noticeably less vertical space while keeping the same readability. (2) Promo lives on the final sell price only. If a section has a Buyer 2 row, the Buyer 1 ADD PROMO sub-block is hidden. If a section has a Buyer 3 row, the Buyer 2 ADD PROMO is hidden. The promo input therefore always sits on the most-downstream sell price in the section (the only sale that's actually subject to a promo). (3) Buyer headings rename in cascade. When Buyer 2 is added, the Buyer 1 row heading changes from "MSRP / Buyer 1 Sell Price" to "Buyer 1 Sell Price", and Buyer 2 is "MSRP / Buyer 2 Sell Price". When Buyer 3 is added, Buyer 2 becomes "Buyer 2 Sell Price" and Buyer 3 is "MSRP / Buyer 3 Sell Price". The headings rebalance automatically the instant Add Buyer / Remove Buyer is clicked. (4) Click-to-copy MC reference value. Left-click any "$X.XX" portion of the (base × multiplier = total) MC reference line (e.g. the $682.80 in "($5.69 × 120 = $682.80)") and that total is copied to clipboard with a 1-second "Copied $682.80" toast right above the cursor. Left-clicking the price input ABOVE that MC reference (in the 10 Pack MC or 40 Pack MC panel) instantly pastes the most recently copied value into the box, replacing any prior value. (5) Per-section custom heading + padlock. Each section heading row in 10 Pack / 40 Pack base panels (Sell Price, MSRP / Buyer 1, MSRP or Buyer 2, MSRP or Buyer 3) carries a faded text input (max 40 chars) sized like the PRODUCER MARGIN label. The Sell Price row's heading auto-fills with the active CUSTOMER CARD's name and is non-editable. Buyer 1 / 2 / 3 heading inputs offer a 3-option picker on click (End User · <Customer Card name> - Chain Store · Enter Text), and once a heading is set a padlock appears on the left. The heading auto-locks on blur. Clicking the padlock re-opens the picker with Edit Text / End User / <Customer Card name> - Chain Store options. (6) Pre-Paid Ship-Calc reconnect. The Ship-Calc panels (4 MC ORDER · 1 MC · 1 PACK · 1 BLADE / PC) were reading the legacy distA-F / msrpA-C fields and showed no data after the C0.4 storage cleanup. They now pull the new section + buyer prices off the matching 10 Pack MC / 40 Pack MC SKU record, iterate every section that exists in the picked profile, and render one row per visible sell price (Sell + Buyer 1 / 2 / 3 as configured) with the freight cost subtracted from the GripBlades side only. The four-scale math (total / 1 MC / 1 pack / 1 blade) is unchanged — it just sources its prices from the new field set. (7) VERSION STAMP. Bumped from C0.4 → C0.5 throughout: title bar (GB-BizCalc - C0.5), version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C0.5', AND the JSON export __version__ field now reads 'C0.5'. The CSV download _<VERSION> suffix automatically advances to "_C0.5". The C0.4, C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.4May 2026Skipped C0.3 (a bug-fix patch over C0.2). Major ROI panel overhaul plus a Customer Card Clipboard click-isolation fix plus the version bump. (1) CUSTOMER CARD CLIPBOARD ghost slots. Clicking a Sub Profile button (A/B/...) inside a ghost slot no longer migrates the scroller's active card / active sub. The ghost card is the same underlying card — name edits, lock toggles, and Sub Profile right-click menu (rename / color / lock) still flow through to the real card — but a left-click on a ghost sub-button is now inert, so you can park a reference card in the clipboard while working on a different active card in the left-right scroll area without losing your spot. (2) ROI panel template rewritten. The fixed-row "1.A-DIST. Cost / GripBlades Margin" and "7.A-MSRP" rows + the "Dist. ROI From Retail Pricing" block on every pack panel (10 Pack, 10 Pack MC, 40 Pack, 40 Pack MC) are replaced with a section + buyer model. A panel defaults to one section. Each section starts with a blue "1." (or 2., 3., ...) indicator on the left of a LANDED / 10 PACK heading (LANDED / 10 Pk MC, LANDED / 40 PACK, or LANDED / 40 Pk MC depending on which panel). To the right of the LANDED heading is the live landed-cost-per-pack or per-MC value from the panel readouts; if the upstream LANDED $/pc value changes, the section-heading number updates to match instantly. (3) Sell Price + PRODUCER MARGIN. The price input below the LANDED heading is labelled "Sell Price (Direct to Retail)" when no Buyer rows exist, and "Sell Price (Buyer 1 Cost)" the moment the first Add Buyer is clicked. The margin readout under the input is now headed "PRODUCER MARGIN" (was "GRIPBLADES MARGIN") and renders side-by-side as "$1.55 | 28.27%" (gray dollar, green / red percent). (4) Conditional ADD PROMO. When the Sell Price is the only price entered in a section, an ADD PROMO heading + % input appears below PRODUCER MARGIN. Typing a number into it produces a MARGIN AFTER PROMO readout in the same "$1.75 | 30.79%" format. The instant a value is typed into MSRP / Buyer 1 Sell Price, this Sell-row promo block hides — the promo logic moves into the Buyer 1 row instead. (5) MSRP / Buyer 1 Sell Price row. Under PRODUCER MARGIN (or under the Sell-row promo readout if one exists) sits the new "MSRP / Buyer 1 Sell Price" (formerly A-MSRP) input. Its margin readout, "MSRP / BUYER 1 MARGIN", calculates from the Sell Price to the MSRP value. A "MARGIN AFTER PROMO" input under that takes a % and produces the discounted-margin readout in the new side-by-side format. (6) Add Buyer (up to Buyer 3). A blue "Add Buyer" hyperlink under the Buyer 1 row spawns "MSRP or Buyer 2 Sell Price" with its own MSRP / BUYER 2 MARGIN (margin between Buyer 1 sell and Buyer 2 sell) and its own MARGIN AFTER PROMO. A second Add Buyer click adds Buyer 3 (margin vs Buyer 2). The Add Buyer link becomes "Remove Buyer" on the most recent buyer row. Clicking Remove Buyer pops a center-screen "CONFIRM TO REMOVE BUYER. !BUYER DATA WILL BE ERASED!" modal. Buyer rows must be removed in reverse order — removing Buyer 1 while Buyer 2 still exists is blocked with a reminder modal. (7) Per-panel "+" section button. The 10 Pack and 40 Pack base panels carry a small + button in the upper left of section 1. Clicking + adds section 2 (and 3, 4, ...) below the current one, separated by a thin black divider and a few rows of white space. Section 2+ also gets a small " - " button to its left; clicking it pops a center-screen "CONFIRM TO COLLAPSE & ERASE PANEL N" modal and on confirm wipes that section's inputs + promos and re-renders. (8) MC panel mirror. The 10 Pack MC and 40 Pack MC panels no longer carry their own + / - buttons. They auto-mirror the base panel: same number of sections, same number of buyer rows per section. Each MC input starts blank (you enter the MC-level price manually). Under each MC input is a small reference value showing the matching base-panel value × the MC multiplier (e.g. "($5.49 × 120 = $658.80)" for a 10 Pack MC). (9) Storage cleanup. On first C0.4 load, every Customer Card × Sub Profile × Macro Cost Basis × MPN record migrates: distA → s1_sell, msrpA → s1_msrp1 (with s1_buyers set to 1 if the MSRP had a value, 0 otherwise). The legacy distB-F, msrpB-D, retail, retailB, retailC fields and any A-D promo letters are permanently wiped from localStorage — not just hidden — so the storage shape exactly matches the new model. After this one-time migration, every input you type into C0.4 autosaves into the new field names per usual. (10) Old headings removed. "Distributor Cost", "GripBlades ROI From MSRP (Direct-to-End-User)", and "Dist. ROI From Retail Pricing" section labels are removed from every pack panel. (11) VERSION STAMP. Bumped from C0.2 (skipping C0.3) → C0.4 throughout: title bar (GB-BizCalc - C0.4), version-badge, all PDF headers/footers, every save-as filename, Save-a-copy HTML download, the news PDF footer string, internal VERSION constants updated to 'C0.4', AND the JSON export __version__ field now reads 'C0.4'. The CSV download _<VERSION> suffix automatically advances to "_C0.4". The C0.2, C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.2May 2026Six changes on top of C0.1 plus the version bump. (1) LANDED $/pc inputs no longer show the up/down spinner buttons. CSS now applies -webkit-appearance:none to the inner/outer spin pseudo-elements and -moz-appearance:textfield to the input itself, so every MACRO COST BASIS option \u2014 built-ins + user-added via + \u2014 shows a clean number field. (2) CUSTOMER CARDS dropdown is now draggable by its title bar. Click and hold the head between "CUSTOMER CARDS" and the X, then drag to position anywhere on screen (including partially off-screen, just like the GB-CALC popup). Position persists in sessionStorage[gbBizCalcCustCardsDdPos_v1] within the session so closing and re-opening keeps it where you left it. Reloading the page (or closing the app) resets back to the default position below the CUSTOMER CARDS button. (3) Toolbar row layout switched to fixed-width clipboards + ghost slots with the Active readout claiming the leftover space. Each ghost CUSTOMER CARD CLIPBOARD slot is now pinned to 182px (same as a real customer card). The Active readout becomes flex:1 1 auto with min-width:0 so long active-card names + profile letters fit on the left without anything else shifting around. (4) The 3rd profile CLIPBOARD button is removed and replaced by a 3rd ghost CUSTOMER CARD CLIPBOARD slot. Final layout: 2 profile CLIPBOARDS + 3 ghost slots. Storage shape migrated from a 2-element array to a 3-element array; existing 2-slot data carries forward intact. (5) ROI-Calc rendering trimmed for the upcoming rebuild. DISTRIBUTOR COST shows only 1. A-DIST. Cost. GRIPBLADES ROI FROM MSRP shows only 7. A-MSRP. DIST. ROI FROM RETAIL PRICING shows only the A-MSRP block with only the vs A-DIST line. The B-F dist / B-C MSRP storage stays in place (no data is deleted from localStorage), they\'re just no longer rendered or editable in the UI. (6) RESET button on every 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panel refuses to clear data when the active Sub Profile is locked. Clicking RESET on a locked profile pops a center-screen alert reading "UNLOCK PROFILE TO CLEAR DATA" and returns early \u2014 nothing in roiState, roiPromos, the active card\'s sub-profile snapshot, or the visible inputs is touched. Unlocking the profile via the profile-button right-click menu restores the standard CONFIRM YOU WANT TO RESET & CLEAR THE DATA flow. (7) VERSION STAMP. Bumped from C0.1 -> C0.2 throughout: title bar (GB-BizCalc - C0.2), version-badge, all PDF headers/footers, every save-as filename (every -C0.1.pdf -> -C0.2.pdf, including cust-info-<company>-notes-only-C0.1.pdf -> -C0.2.pdf and HS-Codes_C0.1.pdf -> HS-Codes_C0.2.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · C0.2" news PDF footer string, internal VERSION constants updated to 'C0.2', AND the JSON export __version__ field now reads 'C0.2'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_C0.2". NO data paths changed beyond the six change-groups above. The C0.1, C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.1May 2026One fix on top of C0.0 plus the version bump. (1) Clicking the body of a customer card pinned in a ghost CUSTOMER CARD CLIPBOARD slot no longer migrates the active highlight in the scroller. Root cause: the ghost-rendered .roi-customer-slot inherited the same body-level onclick="roiSelectCustomerCard(cardId)" as the scroller copy. Removed that handler from the ghost render (plus added cursor:default so the slot no longer looks like a clickable card). Right-click context menu (with the REMOVE FROM CLIPBOARD row when invoked from a ghost slot), name input edits, lock toggle, profile button drag-out and profile right-click all keep working. So now the user can leave DURRIE-ZORO active in the scroller and reference / right-click / drag profiles from a ghost ACE card without losing the scroller\'s active-card highlight. (2) VERSION STAMP. Bumped from C0.0 -> C0.1 throughout: title bar (GB-BizCalc - C0.1), version-badge, all PDF headers/footers, every save-as filename (every -C0.0.pdf -> -C0.1.pdf, including cust-info-<company>-notes-only-C0.0.pdf -> -C0.1.pdf and HS-Codes_C0.0.pdf -> HS-Codes_C0.1.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · C0.1" news PDF footer string, internal VERSION constants updated to 'C0.1', AND the JSON export __version__ field now reads 'C0.1'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_C0.1". NO data paths changed beyond the one fix above. The C0.0, B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
C0.0May 2026First C-series release \u2014 the version scheme has no double-digit segments, so B9.9 advances directly to C0.0. Two change-groups on top of B9.9. (1) The 4th (rightmost) profile CLIPBOARD slot is removed. In its place, 2 ghost CUSTOMER CARD CLIPBOARD slots stretch from where the removed slot was to the far right of the toolbar row. Each slot is the same rounded-outline shape as a customer card (border-radius 8px), filled with a 50%-lighter gray than an inactive CLIPBOARD button (#e8eaed bg, #d4d6db dashed border), and shows the centered, uppercase, letter-spaced text "CUSTOMER CARD CLIPBOARD" in #9ca3af. Empty slots can be dragged onto from the horizontal customer-card scroller \u2014 the existing roiCardDragStart sets _roiDragCardId, and the new roiCardClipboardDrop reads it to pin the cardId into localStorage[gbBizCalcCardClipboard_v1] = [cardId|null, cardId|null]. When a slot is pinned, the slot flips to filled state and renders a live preview of the card (same .roi-customer-slot markup as the scroller, minus DOM IDs to avoid duplicate-id collisions) so all interactions \u2014 name editing, profile button clicks, lock toggling, right-click \u2014 flow through the existing handlers and read/write the same underlying roiCustomerCards / roiCardData state. The user can pin a card in both ghost slots; pinning the same card into the other slot automatically clears the first occurrence to avoid duplicates. The horizontal scroller copy stays put and stays in sync via roiRenderCustomerCards calling roiRenderCardClipboards on every refresh. The same card cannot occupy both ghost slots simultaneously \u2014 dragging it into the other slot auto-clears the first. (2) Right-clicking a pinned customer card in a ghost slot now surfaces the standard customer context menu PLUS a new "REMOVE FROM CLIPBOARD" row prepended at the top in an orange-yellow tone (#fde68a bg, #f59e0b border, #7c2d12 text). Clicking it clears the slot \u2014 the original card stays untouched in the scroller. The remaining four menu rows (DELETE CUST. CARD / CUSTOMER INFO / DUPLICATE CARD / PDF - ALL COST BASIS) work exactly the same regardless of whether the right-click happened in the scroller or a ghost slot. The clipboard context is detected at right-click time by walking the event target\'s closest .roi-card-clipboard-slot ancestor and reading its data-clipboard-slot attribute; the slot index is stashed in _roiCardCtxState.clipSlot so the action handler can route the REMOVE FROM CLIPBOARD branch through _roiClearCardClipboardSlot(slotIdx). (3) VERSION STAMP. Bumped from B9.9 -> C0.0 throughout (B series ends, C series begins per the no-double-digit version rule): title bar (GB-BizCalc - C0.0), version-badge, all PDF headers/footers, every save-as filename (every -B9.9.pdf -> -C0.0.pdf, including cust-info-<company>-notes-only-B9.9.pdf -> -C0.0.pdf and HS-Codes_B9.9.pdf -> HS-Codes_C0.0.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · C0.0" news PDF footer string, internal VERSION constants updated to 'C0.0', AND the JSON export __version__ field now reads 'C0.0'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_C0.0". NO data paths changed beyond the two change-groups above. The B9.9, B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.9May 2026One change on top of B9.8 plus the version bump. (1) Tariff-Calc summary metric blocks now read consistently. The sub-line styling treatment introduced in B9.8 for the LANDED COST block (top sub bold, bottom sub not bold, both smaller in mono font) is now applied to the TOTAL TARIFF COST block (#mTariffSub / #mTariffPerPack, dark text on the gray bg) and the GOODS VALUE (no tariffs) block (#mGoodsSub / #mGoodsPerPack, dark text on the light bg). Across all three blocks: the per-piece line is 13px bold (font-weight 700) and the per-pack line is 13px not-bold (font-weight 500) with opacity .85, both using the monospace family. Each block keeps its own block-appropriate color (off-white on the black LANDED bg, near-black on the gray TARIFF bg, rgba(0,0,0,.85) on the light GOODS bg) so the three cards balance visually. (2) VERSION STAMP. Bumped from B9.8 -> B9.9 throughout: title bar (GB-BizCalc - B9.9), version-badge, all PDF headers/footers, every save-as filename (every -B9.8.pdf -> -B9.9.pdf, including cust-info-<company>-notes-only-B9.8.pdf -> -B9.9.pdf and HS-Codes_B9.8.pdf -> HS-Codes_B9.9.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.9" news PDF footer string, internal VERSION constants updated to 'B9.9', AND the JSON export __version__ field now reads 'B9.9'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.9". NO data paths changed beyond the one styling change above. The B9.8, B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.8May 2026One fix on top of B9.7 plus the version bump. (1) Tariff-Calc LANDED COST block sub-line styling fixed. The "$0.3938 per piece" sub-line (#mLandedSub) on the black-background LANDED COST block was rendering as effectively black-on-black because a lower-specificity .metric-sub rule was winning over the .metric.dark-hero .metric-sub default in some inherited stylesheet path. B9.8 adds an explicit id-targeted rule with !important so #mLandedSub now renders as off-white (rgba 255,255,255,.92), bold (font-weight 700), and slightly smaller (13px) so it visually aligns with the per-pack subline beneath it. The "$3.9376 per 10 Pack" sub-line (#mLandedPerPack) gets a matching explicit override: smaller (13px), NOT bold (font-weight 500), opacity .85 \u2014 matching the per-pack subline styling on the gray-background TOTAL TARIFF COST block for visual parity. (2) VERSION STAMP. Bumped from B9.7 -> B9.8 throughout: title bar (GB-BizCalc - B9.8), version-badge, all PDF headers/footers, every save-as filename (every -B9.7.pdf -> -B9.8.pdf, including cust-info-<company>-notes-only-B9.7.pdf -> -B9.8.pdf and HS-Codes_B9.7.pdf -> HS-Codes_B9.8.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.8" news PDF footer string, internal VERSION constants updated to 'B9.8', AND the JSON export __version__ field now reads 'B9.8'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.8". NO data paths changed beyond the one fix above. The B9.7, B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.7May 2026Three changes on top of B9.6 plus the version bump. (1) CUSTOMER CARDS dropdown caps at \u224814 visible rows then scrolls. The search input lives in a separate container above the scrolling list, so it stays put at the top regardless of how far down the user scrolls inside the list. .roi-cust-dd-list max-height was 600px (\u224820 rows); now 430px (\u224814 rows at ~30px each). (2) Vietnam and India added to the Country of Export dropdown, slotted between Taiwan and United States. Internal codes are 'VN' and 'IN'. The origin-label ternary chains used by the live AI tariff fetcher prompts now translate VN -> "Vietnam" and IN -> "India", so UPDATE TARIFFS sends meaningful labels to the AI for every import country (US, CA, MX). VN and IN are MFN-only origins for all three destinations \u2014 no Section 301, no China Surtax Order, no IEPS emergency surcharge \u2014 plus whatever bilateral anti-dumping or countervailing duties the AI surfaces for specific HTS lines on the live call. User Guide section 3 (Tariff-Calc Interface Guide), section 7 (Inputs origin-list), section 8 (Tariff Calculation Engine — new subsection "Vietnam & India Imports"), and section 9 (Each Tariff Explained — inline note above the route sub-sections) updated to reflect the new origins. (3) VERSION STAMP. Bumped from B9.6 -> B9.7 throughout: title bar (GB-BizCalc - B9.7), version-badge, all PDF headers/footers, every save-as filename (every -B9.6.pdf -> -B9.7.pdf, including cust-info-<company>-notes-only-B9.6.pdf -> -B9.7.pdf and HS-Codes_B9.6.pdf -> HS-Codes_B9.7.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.7" news PDF footer string, internal VERSION constants updated to 'B9.7', AND the JSON export __version__ field now reads 'B9.7'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.7". NO data paths changed beyond the two feature changes above. The B9.6, B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.6May 2026One fix on top of B9.5 plus the version bump. (1) CUSTOMER CARDS dropdown panel was opening at the bottom of the screen instead of right below the button. Root cause: the panel is rendered as a sibling of the .roi-customer-title span, not a child, so position:absolute resolved against a far positioned ancestor and the static top:calc(100% + 6px) dropped it near the bar bottom. B9.6 fix: switch the panel to position:fixed and compute its left/top in JS on every open from the CUSTOMER CARDS button\'s getBoundingClientRect(), with viewport clamping so wide panels never overflow right or below the screen. The panel now appears immediately under the button regardless of how the parent bar is laid out. (2) VERSION STAMP. Bumped from B9.5 -> B9.6 throughout: title bar (GB-BizCalc - B9.6), version-badge, all PDF headers/footers, every save-as filename (every -B9.5.pdf -> -B9.6.pdf, including cust-info-<company>-notes-only-B9.5.pdf -> -B9.6.pdf and HS-Codes_B9.5.pdf -> HS-Codes_B9.6.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.6" news PDF footer string, internal VERSION constants updated to 'B9.6', AND the JSON export __version__ field now reads 'B9.6'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.6". NO data paths changed beyond the one fix above. The B9.5, B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.5May 2026One change-group on top of B9.4 plus the version bump. (1) CUSTOMER CARDS heading is now a dropdown button. Clicking the "CUSTOMER CARDS" chip in the ROI-Calc bar opens a small panel anchored below it containing: a non-editable "CUSTOMER CARDS" heading, a search box, and a vertically-scrolling list of every customer card in alphabetical order. The displayed name auto-appends "(HQ)" whenever the card's HQ checkbox is on. The list shows ~20 rows at a time and scrolls when there are more. Typing in the search box filters the list live (case-insensitive substring match). Clicking a row activates that card just like clicking its slot in the horizontal scroller, and then auto-scrolls the existing left-to-right Customer Cards scroller so the activated card sits at the FAR LEFT of the visible area (scroller.scrollTo with smooth behavior). The horizontal scroller keeps its drag-and-drop reorderability \u2014 the alphabetical view is only for the dropdown, not the underlying card order. Dropdown closes on outside click or Escape. (2) VERSION STAMP. Bumped from B9.4 -> B9.5 throughout: title bar (GB-BizCalc - B9.5), version-badge, all PDF headers/footers, every save-as filename (every -B9.4.pdf -> -B9.5.pdf, including cust-info-<company>-notes-only-B9.4.pdf -> -B9.5.pdf and HS-Codes_B9.4.pdf -> HS-Codes_B9.5.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.5" news PDF footer string, internal VERSION constants updated to 'B9.5', AND the JSON export __version__ field now reads 'B9.5'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.5". NO data paths changed beyond the one change-group above. The B9.4, B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.4May 2026One change-group on top of B9.3 plus the version bump. (1) MACRO COST BASIS LOG button overhaul. The Last Value Logged label + value chip is removed from every basis option (Pallet, TEU 20 FT, FEU 40 FT, and any user-added custom basis). The LOG button stays, but now opens a center-screen popup listing the last 10 logged values for that basis \u00d7 active pack-size subtab, each row showing the value and a date+time stamp. Clicking a row pops a confirm modal "CONFIRM TO CHANGE THE $<current> VALUE TO $<clicked>". YES swaps: the clicked value becomes the new LANDED $/pc input, and the previous LANDED $/pc value is pushed back into the log at the top. The log also auto-fills on every LANDED $/pc edit \u2014 when the user changes the value, the previous value is pushed to the log automatically (via a head-of-function shim in roiBasisLandedInput) so the history builds up naturally without an explicit "capture" click. Cap at 10 entries per basis \u00d7 subtab; older entries fall off the bottom. Storage shape changed from roiBasisLog[basis][subtab] = "0.3500" (single string) to roiBasisLog[basis][subtab] = [{value, ts}, ...] (array, newest first). Pre-B9.4 single-string entries auto-migrate to a single-entry array on load so existing data is preserved. The Customer Card x Profile snapshot machinery is unchanged \u2014 the log is global per-basis-subtab, not per-profile. (2) VERSION STAMP. Bumped from B9.3 -> B9.4 throughout: title bar (GB-BizCalc - B9.4), version-badge, all PDF headers/footers, every save-as filename (every -B9.3.pdf -> -B9.4.pdf, including cust-info-<company>-notes-only-B9.3.pdf -> -B9.4.pdf and HS-Codes_B9.3.pdf -> HS-Codes_B9.4.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.4" news PDF footer string, internal VERSION constants updated to 'B9.4', AND the JSON export __version__ field now reads 'B9.4'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.4". NO data paths changed beyond the one change-group above. The B9.3, B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.3May 2026Three change-groups on top of B9.2 plus the version bump. (1) HS CODE HISTORY LOG: ADD BACK TO LIST works again. The panel\'s outside-click handler was dismissing the panel mid-click on the floating restore tip, so the restore completed in storage but the panel was gone before the user could see it. The B9.3 fix adds the tip + new context menu to the panel\'s blocking-ids list so clicks on either never dismiss the panel. Also pulled the restore code into a shared _htsRestoreLogEntry(id) helper so the legacy tip + new menu both move the entry from the deleted ring back to the top of the active list, preserving locked / details / name fields. (2) New multi-option HS row context menu. Right-click on any row \u2014 active or history-log \u2014 now opens a small menu at the cursor with: USE THIS HS CODE (both views), ADD BACK TO LIST (log only), DELETE LOG (log only), DELETE (active only; suppressed on locked rows). USE THIS HS CODE pops a center-screen confirm modal "ARE YOU SURE YOU WANT TO SWAP HS CODES?" with an injected "Replacing <old> with <new code — name>." subline; YES copies the row's code into the htsCode input and (when populated) the row's name into productName. DELETE LOG pops "CONFIRM TO DELETE THIS LOG"; YES permanently removes that one entry from the deleted-history ring buffer. DELETE keeps the B9.2 "CONFIRM TO DELETE HS CODE" flow. The menu hides on outside click and is clamped to viewport bounds. (3) Tariff-Calc summary metric blocks swapped. LANDED COST now sits on the LEFT with the black-hero look (white text on #1c1c1e); TOTAL TARIFF COST sits in the MIDDLE with the gray-hero look (black text on #e2e2e6). The class names .dark-hero / .landed-hero are unchanged \u2014 only the two <div class="metric ..."> blocks were rearranged so the styling pairs with the new label. Goods Value block on the right is unchanged. (4) VERSION STAMP. Bumped from B9.2 -> B9.3 throughout: title bar (GB-BizCalc - B9.3), version-badge, all PDF headers/footers, every save-as filename (every -B9.2.pdf -> -B9.3.pdf, including cust-info-<company>-notes-only-B9.2.pdf -> -B9.3.pdf and HS-Codes_B9.2.pdf -> HS-Codes_B9.3.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.3" news PDF footer string, internal VERSION constants updated to 'B9.3', AND the JSON export __version__ field now reads 'B9.3'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.3". NO data paths changed beyond the three change-groups above. The B9.2, B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.2May 2026Two changes on top of B9.1 plus the version bump. (1) Distributor Cost + MSRP + promo % inputs are now SHARED across every MACRO COST BASIS profile within a pack panel. Switching from PALLET to TEU 20 FT or FEU 40 FT (or any user-added custom basis) keeps the A\u2013F Dist Cost values, the A/B/C MSRP values, and the per-MSRP promo percentages identical. Only the landed-cost-driven margins (GripBlades Margin %, $ Profit, Retail ROI margins, etc.) still recompute per basis, since they depend on the basis-specific landed-$/pc value. Implementation: four helpers were added \u2014 _roiMirrorInputAllBases(mpn, field, value), _roiMirrorPromoAllBases(mpn, letter, pct), _roiNormalizeStateAcrossBases(state), and _roiNormalizePromosAcrossBases(promos). Every write path (grid input listener, roiSaveInputsToState, roiAddPromo, roiClearCardInputs, roiApplySnapshot) now mirrors writes to every basis in ROI_BASES. Every read path is unchanged \u2014 roiState[basis][mpn][field] and roiPromos[basis][mpn][L] are still the source of truth, so all downstream code (roiRecalc, PDF/CSV exports, customer-card snapshots) keeps working without per-call rewrites. On initial roiBuild and on every Customer-Card / Sub-Profile switch (roiLoadActiveIntoVisible), a normalization pass merges any pre-B9.2 per-basis data into a single shared record so existing customer profiles immediately become shared. The Clear Profile flow clears every basis at once; the Apply Snapshot flow writes the restored values to every basis. (2) VERSION STAMP. Bumped from B9.1 -> B9.2 throughout: title bar (GB-BizCalc - B9.2), version-badge, all PDF headers/footers, every save-as filename (every -B9.1.pdf -> -B9.2.pdf, including cust-info-<company>-notes-only-B9.1.pdf -> -B9.2.pdf and HS-Codes_B9.1.pdf -> HS-Codes_B9.2.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.2" news PDF footer string, internal VERSION constants updated to 'B9.2', AND the JSON export __version__ field now reads 'B9.2'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.2". NO data paths changed beyond the one feature change above. The B9.1, B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.1May 2026Four changes on top of B9.0 plus the version bump. (1) Two new Distributor Cost rows added to every pack panel (10 Pack, 10 Pack MC, 40 Pack, 40 Pack MC): "5. E-DIST. Cost" and "6. F-DIST. Cost". Saved under storage keys 'distE' and 'distF' alongside the existing distA/B/C/D. Empty by default for backward compatibility — any pre-existing saved data continues to load as-is, with the new E + F cells just rendering blank until the user types into them. (2) The MSRP section renumbers to 7 / 8 / 9 (was 5 / 6 / 7) because the Dist Cost section now occupies positions 1\u20136. Block labels become "7. A-MSRP", "8. B-MSRP", "9. C-MSRP". The data field names (msrpA/B/C) are unchanged so existing saved data still loads. (3) Every A/B/C/D/E/F letter prefix after each row number is wrapped in <strong>, so the letter renders bold inside the "1. A-DIST. Cost" \u2192 "6. F-DIST. Cost" labels and "7. A-MSRP" \u2192 "9. C-MSRP" labels. Same treatment for the "vs A-DIST" rows inside DIST. ROI FROM RETAIL PRICING. (4) E + F flow through everything that already drove A/B/C/D: ROI_FIELDS and _ROI_INPUT_FIELDS gain 'distE'/'distF'; roiRecalc's GripBlades-margin loop now iterates A\u2013F; the per-row retail ROI table renders "vs E-DIST" and "vs F-DIST" alongside A\u2013D inside each A/B/C MSRP block; the 10 Pk MC / 40 Pk MC totals row (new in B9.0) now also renders E + F totals; the basis-state hydrate path's distPrices map and the PDF/CSV exports' distVals + distLetters arrays both extend to E + F; the Pre-Paid Ship-Calc dist iteration also covers E + F. (5) VERSION STAMP. Bumped from B9.0 -> B9.1 throughout: title bar (GB-BizCalc - B9.1), version-badge, all PDF headers/footers, every save-as filename (every -B9.0.pdf -> -B9.1.pdf, including cust-info-<company>-notes-only-B9.0.pdf -> -B9.1.pdf and HS-Codes_B9.0.pdf -> HS-Codes_B9.1.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.1" news PDF footer string, internal VERSION constants updated to 'B9.1', AND the JSON export __version__ field now reads 'B9.1'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.1". NO data paths changed beyond the four feature changes above. The B9.0, B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B9.0May 2026One change on top of B8.9 plus the version bump (B8.10 is intentionally skipped since the version scheme has no double-digit segments; B8.9 advances directly to B9.0). (1) 10 Pack MC and 40 Pack MC panels now render an extra readout block to the right of the GRIPBLADES MARGIN block on every A/B/C/D Dist Cost row. The new block\'s heading reads "10Pk MC (xN)" or "40Pk MC (xN)" where N is the live packsPerMC value from the SHIPPING REFERENCE chart (read via the existing ROI_PACKS_PER_MC[sku.pack] proxy, so 120 for 10-pack MC and 24 for 40-pack MC by default, or whatever the user has set via the SHIPPING REFERENCE EDIT CHART flow). The dollar value under the heading is computed live as (source Pack panel\'s same-letter Dist Cost) \u00d7 N. Example: if the 10 Pack panel has A-DIST. Cost = $5.49, the 10 Pack MC panel\'s A-DIST. Cost row now shows "$658.80" under its "10Pk MC (x120)" block. The mapping is letter-for-letter (10 Pack A \u2192 10 Pack MC A, 10 Pack B \u2192 10 Pack MC B, etc., and same for 40-pack pair). New helpers _roiFindMcSibling(packIdx) + _roiUpdateMcTotals(mcIdx) drive the math; roiRecalc(i) was extended so editing either the Pack panel\'s dist input or the MC panel\'s own dist input refreshes the MC total cells on every keystroke. When SHIPPING REFERENCE chart values change (Edit \u2192 Lock cycle), the existing _srfRefreshDependents() pipeline already calls roiRebuild() which re-renders the headings + recomputes the totals, so the (xN) callout auto-updates without any extra wiring. The Pack panels (10 Pack and 40 Pack rows) are unchanged — the new block only appears on MC rows. (2) VERSION STAMP. Bumped from B8.9 -> B9.0 throughout: title bar (GB-BizCalc - B9.0), version-badge, all PDF headers/footers, every save-as filename (every -B8.9.pdf -> -B9.0.pdf, including cust-info-<company>-notes-only-B8.9.pdf -> -B9.0.pdf and HS-Codes_B8.9.pdf -> HS-Codes_B9.0.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B9.0" news PDF footer string, internal VERSION constants updated to 'B9.0', AND the JSON export __version__ field now reads 'B9.0'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B9.0". NO data paths changed beyond the one feature change above. The B8.9, B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.9May 2026Three changes on top of B8.8 plus the version bump. The other four items the user asked for (sharing MACRO COST BASIS Dist / MSRP inputs across profiles, adding E + F Dist Cost rows + bolding the letter prefix, adding 10/40 Pk MC totals next to GRIPBLADES MARGIN on the MC panels, and wiring Vietnam + India into Country of Export) need a deeper data-model investigation than safely fit in this batch and will land in a follow-up build. (1) CONFIRM TO REMOVE FROM FAVORITES popup buttons swapped — YES is on the LEFT and NO is on the RIGHT, matching the LOAD CHART confirm popup convention. Wiring is unchanged: YES still runs the pending favorite-removal callback, NO closes without removing. Used by both the TariffNews favorites flow and the SHIPPING REFERENCE chart favorites flow. (2) CHART HISTORY rename now works for every non-Legacy / non-Current row. Each LAST USED / LAST LAST USED / LAST LAST LAST USED row\'s name span gets ondblclick="_srfHistStartRename(...)\" (already-existing helper) + a "Double-click to rename (max 55 chars)" title. Once renamed, the user\'s name takes priority over the positional label, so a chart the user calls "Ace HQ MAIN" keeps that name even when its position in the cycle shifts. The LEGACY CHART and CURRENT CHART rows still have no rename hook (CURRENT always reads "CURRENT CHART", LEGACY always reads "LEGACY CHART"). (3) Deleting a chart now preserves its current displayed name. When srfDeleteFromHistory or _srfHistDeleteSelected moves a row into the Last Deleted Charts ring, the row\'s positional label (LAST USED CHART, LAST LAST USED CHART, LAST LAST LAST USED CHART) is snapshotted into item.name first (only if the user hadn\'t already renamed it), so the deleted-list shows the same label the user saw moments before deleting. If the user had renamed the chart, that user-set name is preserved as-is. Restored entries keep the name too. (4) VERSION STAMP. Bumped from B8.8 -> B8.9 throughout: title bar (GB-BizCalc - B8.9), version-badge, all PDF headers/footers, every save-as filename (every -B8.8.pdf -> -B8.9.pdf, including cust-info-<company>-notes-only-B8.8.pdf -> -B8.9.pdf and HS-Codes_B8.8.pdf -> HS-Codes_B8.9.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.9" news PDF footer string, internal VERSION constants updated to 'B8.9', AND the JSON export __version__ field now reads 'B8.9'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.9". NO data paths changed beyond the three feature changes above. The B8.8, B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.8May 2026Three changes on top of B8.7 plus the version bump. (1) MFR / LANDED Cost Ref. Chart NOTES popup loses the Cancel + Save row and switches to autosave. gbChartNotesTextarea has oninput="gbSaveChartNotes()" so every keystroke writes the current text to localStorage[gbBizCalcChartNotes_v1]. The X close button (or backdrop click) just hides the popup — the data is already persisted by the time the popup closes. Behaves like the existing BUILD NOTES pad. (2) HS row NOTES popup (was DETAILS) gets the same treatment. The Cancel + Save row is gone, the textarea autosaves on every keystroke (per-row entry.details for rows opened via the per-row NOTES button, or per-HS-code fallback for the legacy gbShowProductDetails entry-point). gbSaveRowDetails / gbSaveProductDetails no longer call gbHideProductDetails, _htsRenderPanel, or window.alert from inside their save path — those are reserved for explicit close. The popup heading is renamed from "PRODUCT / PART DETAILS" to "PRODUCT / PART NOTES", and the per-row button text + title are renamed from DETAILS to NOTES. The CSS class .hts-row-details-btn is kept for selector compatibility but the visible label is now NOTES. The HS panel re-renders exactly once on gbHideProductDetails so the per-row button\'s has-notes tint refreshes after the user closes the popup. (3) LOAD CHART confirm modal button order swapped back: YES is on the LEFT and NO is on the RIGHT, restoring the B8.0 / B8.4 layout (the B8.5/B8.7 swap to NO-left is reverted per user request). Wiring is unchanged — srfLoadConfirmYes still fires the load. (4) VERSION STAMP. Bumped from B8.7 -> B8.8 throughout: title bar (GB-BizCalc - B8.8), version-badge, all PDF headers/footers, every save-as filename (every -B8.7.pdf -> -B8.8.pdf, including cust-info-<company>-notes-only-B8.7.pdf -> -B8.8.pdf and HS-Codes_B8.7.pdf -> HS-Codes_B8.8.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.8" news PDF footer string, internal VERSION constants updated to 'B8.8', AND the JSON export __version__ field now reads 'B8.8'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.8". NO data paths changed beyond the three feature changes above. The B8.7, B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.7May 2026Three changes on top of B8.6 plus the version bump. (1) NOTES button added to the MFR / LANDED Cost Ref. Chart header. The new yellow button sits to the LEFT of the orange Download PDF button. Clicking it opens a center-screen popup (.gb-chart-notes-modal) with a textarea pre-filled from any previously saved notes for the active chart view. SAVE writes the notes to localStorage[gbBizCalcChartNotes_v1] keyed by view (\'mfr\' or \'landed\') so the MFR chart and the LANDED chart each keep their own independent notes. The popup title flips to \"MFR CHART NOTES\" or \"LANDED CHART NOTES\" depending on which chart is currently active. CANCEL just closes without saving. (2) LOAD CHART click flow fixed. Before B8.7, clicking LOAD CHART on a chart-history row would do nothing for some users because the row buttons were wired to ondblclick (a stale requirement from earlier in the B8.x series). Both the regular-row and the LEGACY-row LOAD CHART buttons are now wired to plain onclick, so a single click opens the confirm modal as expected. (3) LOAD CHART confirm modal stacking order fixed. The srf-load-bg / srf-load-modal lived at z-index 2100 / 2110 while the chart preview modal lived at 2150, so opening LOAD CHART from inside an open preview rendered the confirm BEHIND the preview — invisible until the preview was closed. srf-load-bg / srf-load-modal are now z-index 2400 / 2410, and the related "CHART IS ALREADY LOADED" modal (gb-already-bg / gb-already-modal) is bumped to 2500 / 2510, so both confirms always render on top of every other panel or popup that may be open underneath. (4) VERSION STAMP. Bumped from B8.6 -> B8.7 throughout: title bar (GB-BizCalc - B8.7), version-badge, all PDF headers/footers, every save-as filename (every -B8.6.pdf -> -B8.7.pdf, including cust-info-<company>-notes-only-B8.6.pdf -> -B8.7.pdf and HS-Codes_B8.6.pdf -> HS-Codes_B8.7.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.7" news PDF footer string, internal VERSION constants updated to 'B8.7', AND the JSON export __version__ field now reads 'B8.7'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.7". NO data paths changed beyond the three feature changes above. The B8.6, B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.6May 2026One fix on top of B8.5 plus the version bump. (1) Product / Part Name input person + arrow (contact autofill) icon — really gone this time. The B8.5 attribute combination (autocomplete=off + aria-autocomplete=none + data-form-type=other + data-1p/bwignore + role=textbox + no "name" attribute) was not enough to defeat Safari\'s pattern-matching on the "Name" label/placeholder text. Switched to the readonly-on-load trick: the input now starts with the readonly attribute, so the browser refuses to paint the autofill icon at all. JS handlers onmousedown="this.removeAttribute('readonly')" + onfocus="this.removeAttribute('readonly')" strip readonly the instant the user clicks/tabs into the field, so typing works as before. onblur re-applies readonly so the icon stays gone after the user clicks somewhere else. A hidden decoy <input> sits directly above the real field at position:absolute;left:-9999px so any leftover autofill heuristics target the decoy instead. Combined with the existing autocomplete=off + aria-autocomplete=none + no name attribute setup from B8.5, the Safari person+arrow icon and Chrome contact picker are both fully suppressed. (2) VERSION STAMP. Bumped from B8.5 -> B8.6 throughout: title bar (GB-BizCalc - B8.6), version-badge, all PDF headers/footers, every save-as filename (every -B8.5.pdf -> -B8.6.pdf, including cust-info-<company>-notes-only-B8.5.pdf -> -B8.6.pdf and HS-Codes_B8.5.pdf -> HS-Codes_B8.6.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.6" news PDF footer string, internal VERSION constants updated to 'B8.6', AND the JSON export __version__ field now reads 'B8.6'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.6". NO data paths changed beyond the readonly-trick fix above. The B8.5, B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.5May 2026Five changes on top of B8.4 plus the version bump. (1) HS CODE HISTORY now opens via a hyperlink instead of a button. The yellow SELECT button next to the HS Code input is removed; in its place a small "\u2014 (See History Panel)" link sits to the right of the "HTS / HS Code" heading. Style mirrors the existing "\u2014 (See Chart Here)" link beside SEA FREIGHT METHOD: 10px, accent-blue, no underline, cursor:pointer. Clicking it calls htsShowPanel() so the panel still pops up over the SHIPPING REFERENCE area. The .hts-input-wrap right-padding shrunk back from 120px to default since there is no inline button anymore. (2) Product / Part Name input no longer surfaces the macOS / Safari contact-picker icon (the little person + down arrow on the right of the box). Root cause was the name="gb_product_part_name" attribute — Safari pattern-matches "name" inside the input name to offer contact autofill. Fixed by removing the name= attribute entirely and adding aria-autocomplete="none" + role="textbox" + autocomplete="off" (was "new-password"), plus data-1p-ignore + data-bwignore to short-circuit common password manager UIs. (3) New PDF download mode "CUST. INFO - NOTES ONLY" in the SELECT PDF DOWNLOAD OPTIONS menu. Sits directly under "CUST. INFO - NO NOTES". Builds a focused PDF: Company Name in big bold at the top, then a CONTACT 1 block with the contact\'s full name, every email that has text, and every phone that has a number (each with its country code + line type). Empty email / phone slots are skipped. After that comes the COMPANY NOTES block (the notes attached to the NOTES button next to Company Name and the Cust-Info PDF button), then last the CONTACT 1 NOTES block (notes attached to the NOTES button on the Contact 1 row). Filename: cust-info-<company>-notes-only-B8.5.pdf. (4) Removing a TariffNews favorite now requires a confirm. Clicking the yellow star on a favorited article opens a center-screen modal reading "CONFIRM TO REMOVE ARTICLE FROM FAVORITES" with NO / YES buttons. Only YES actually unstars and removes the article from the favorites view. Adding a favorite (clicking a gray star) is unchanged — no confirm needed. The shared modal also drives the chart-history flow. (5) Removing a SHIPPING REFERENCE chart favorite gets the same treatment. Clicking the yellow star on any favorited chart row in CHART HISTORY (or in the chart preview / favorites popup) opens the same shared center-screen modal with the message "CONFIRM TO REMOVE FROM FAVORITES". YES un-stars; NO leaves the favorite untouched. Adding a favorite still adds immediately. New gbAskRemoveFav / gbConfirmFavRemove / gbHideFavRemove helpers shared by both flows so any future favorite system can hook in by passing a callback. (6) VERSION STAMP. Bumped from B8.4 -> B8.5 throughout: title bar (GB-BizCalc - B8.5), version-badge, all PDF headers/footers, every save-as filename (every -B8.4.pdf -> -B8.5.pdf, including cust-info-<company>-notes-only-B8.4.pdf -> -B8.5.pdf and HS-Codes_B8.4.pdf -> HS-Codes_B8.5.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.5" news PDF footer string, internal VERSION constants updated to 'B8.5', AND the JSON export __version__ field now reads 'B8.5'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.5". NO data paths changed beyond the five feature changes above. The B8.4, B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.4May 2026Three rounds of layered fixes on top of the initial B8.2 plus two interim follow-ups, then the version bump from B8.2 -> B8.4 (B8.3 intentionally skipped). (1) productName input pre-populated with "GripBlades™ - Original" by default. The DETAILS button no longer sits next to the input (it later moved into the HS panel header, then again into per-row buttons in round-3). (2) HS CODE HISTORY panel ~2x wider on top of the original B8.2 width, then another ~35% wider in round-3, so .hts-hist-panel min-width is now 1030px / max-width 1485px. The HS code cells stay at their original width while the Product / Part Name cells flex-fill the new horizontal space. (3) HS code input cells got a faded "Enter HS Code" placeholder when blank. The placeholder vanishes the instant the user starts typing. (4) Drag-and-drop reorder for HS rows. Original implementation in round-2 attached draggable=true to the row, but inner <input> elements ate the dragstart so dragging never began. Round-3 fixed this by moving draggable=true onto the \u2630 handle and keeping the row as the drop target. The row flashes a green line above or below to mark the drop point while a row is being hovered. The reorder is persisted to localStorage[gbBizCalcHtsHistory_v1]. Locked rows are not draggable. (5) DETAILS button moved off the panel header into a per-row yellow button on the far right of every HS row. Each row keeps its own notes (entry.details). Rows with notes saved get a darker yellow tint on their DETAILS button. The HS-Codes_VER.pdf export reads entry.details first (with fallback to the legacy per-HS-code map) so per-row notes flow into the "Short + Long Descriptions" PDF too. (6) HISTORY LOG button shortened to HISTORY to leave room in the panel header. The Active List toggle still flips the label when viewing the deleted log. (7) Saving DETAILS no longer dismisses the entire HS CODE HISTORY panel. The outside-click auto-hide guard now skips whenever an overlay modal is open (gbProductDetailsBg/Modal, gbHtsDeleteBg/Modal, srfLoadBg/Modal, gbAlreadyLoadedBg/Modal, or htsHistPdfMenu) so the HS panel stays put underneath while the user finishes the dialog. (8) HS CODE HISTORY panel is draggable again, this time without the previous "jumps an inch southeast on grab" offset bug. Root cause was that the panel was position:absolute relative to the input wrapper, but the drag math used getBoundingClientRect (viewport coords) plus window.scrollX/Y, which only matched when the panel was a direct child of the body. Fixed by switching the panel to position:fixed and using pure viewport coordinates everywhere. Drag handlers re-attached on the .hts-hist-head bar in init(); the user grabs anywhere in the header that isn\'t a button (the area to the right of the "HS CODE HISTORY" words is the obvious grab zone). The connector arrow under the SELECT button hides itself once the user drags the panel off the default anchor. Position is saved to sessionStorage[gbBizCalcHtsPanelPos_v1] so closing and re-opening keeps it where you left it; reloading the page resets back to the default anchor above the SELECT button. (9) Right-clicking a LOCKED HS row continues to do nothing. Right-clicking an UNLOCKED row surfaces a small floating "DELETE" chip near the cursor; clicking it opens the centered "CONFIRM TO DELETE HS CODE" modal. (10) HS Codes PDF kept the wider columns + version stamp from the previous B8.2 work; filename now reads HS-Codes_B8.4.pdf, and the top-right version stamp reads "GB-BizCalc · B8.4". (11) VERSION STAMP. Bumped from B8.2 -> B8.4 throughout (B8.3 intentionally skipped): title bar (GB-BizCalc - B8.4), version-badge, all PDF headers/footers, every save-as filename (every -B8.2.pdf -> -B8.4.pdf, including HS-Codes_B8.2.pdf -> HS-Codes_B8.4.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.4" news PDF footer string, internal VERSION constants updated to 'B8.4', AND the JSON export __version__ field now reads 'B8.4'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.4". NO data paths changed beyond the feature changes above. The B8.2, B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.2May 2026Five changes on top of B8.1 plus the version bump. (1) Right-clicking a LOCKED HS CODE HISTORY row now does absolutely nothing — htsActiveContextMenu returns false before showing any menu, so the row cannot be deleted until you click the padlock to unlock it. Defense-in-depth check in gbConfirmHtsDelete also refuses to delete a row that becomes locked between the right-click and the OK click. (2) Right-clicking an UNLOCKED row no longer pops the native window.confirm. Instead, a small red floating "DELETE" chip (.hts-delete-tip) appears just above the cursor where the right-click happened, tracking the row id via data-id. The user has to physically move the cursor onto that chip and click it. Clicking it then opens a center-screen modal (gbHtsDeleteModal) reading "CONFIRM TO DELETE HS CODE" followed by the <code> \u2014 <product/part name> line, with CANCEL on the left and OK on the right. Only OK actually performs the delete. The chip auto-hides on any document mousedown outside it. (3) SELECT button moved left of the input\'s right edge. CSS .hts-select-btn changed from right:4px to right:42px so it no longer sits flush against the field border, and the htsCode input\'s padding-right bumped from 78px to 120px so typed digits never run underneath the button. (4) HS Codes PDF column layout widened. The COL_NAME_X anchor moved from M+90 to M+200 so the Product / Part Name column has substantially more breathing room from the HS / HTS Code column. Long product names now wrap inside a generous (W - M) - COL_NAME_X width window instead of running off the right edge. New column headings (#, HS / HTS CODE, PRODUCT / PART NAME) plus an underlined divider on every page, and a faint dotted divider between rows so the table reads cleanly. (5) HS Codes PDF now stamps the GB-BizCalc software version (B8.2) at the top-right of every page, in bold 11pt next to the report title — easy to spot which version of the app produced the export. (6) VERSION STAMP. Bumped from B8.1 -> B8.2 throughout: title bar (GB-BizCalc - B8.2), version-badge, all PDF headers/footers, every save-as filename (every -B8.1.pdf -> -B8.2.pdf, including HS-Codes_B8.1.pdf -> HS-Codes_B8.2.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.2" news PDF footer string, internal VERSION constants updated to 'B8.2', AND the JSON export __version__ field now reads 'B8.2'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.2". NO data paths changed beyond the five feature changes above. The B8.1, B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.1May 2026Ten changes on top of B8.0 plus the version bump. (1) HS CODE HISTORY rows gained a per-row lock icon. Click the open padlock to lock the row; the code + product name inputs flip to readonly with a gray background. Click the closed padlock to unlock and edit again. Lock state persists per-row in localStorage. (2) HS CODE HISTORY name input placeholder is now "Product / Part Name…" instead of "Product name…". (3) Product Name input on the Tariff-Calc tab renamed to "Product / Part Name" (label + placeholder), browser address-autofill UI suppressed via autocomplete="new-password" + name="gb_product_part_name" + autocorrect=off + autocapitalize=off + spellcheck=false + data-form-type=other + data-lpignore=true (so the macOS / Chrome "person icon + down arrow" address picker no longer pops up). New yellow DETAILS button on the right opens a center-screen popup with a notes textarea — descriptions, manufacturing tips, importing remarks, supplier comments etc. — keyed off the current HS / HTS code (localStorage[gbBizCalcHtsDetails_v1]). (4) HS CODE HISTORY panel header gained an orange PDF button. Clicking it opens a small dropdown with two options: "HS Codes w/ Short Descriptions" (code + product name only) and "HS Codes w/ Short & Long Descriptions" (also embeds each code\'s saved DETAILS notes block). Both download as HS-Codes_B8.1.pdf via jsPDF. (5) NO / YES button order on the "CONFIRM TO LOAD AND REPLACE EXISTING CHART" modal swapped — NO is now on the left, YES on the right (matches the rest of the app\'s confirm modals). (6) LEGACY CHART row in CHART HISTORY gained a green LOAD CHART button (same green-on-white style as CURRENT-replacing rows). The CURRENT CHART row intentionally still does NOT have a LOAD CHART button — it\'s already loaded by definition. (7) Clicking LOAD CHART when the chart is already the CURRENT one now opens a center-screen modal that reads "CHART IS ALREADY LOADED" with an OK button (replaces the previous window.alert). (8) Chart preview modal title now uses the positional name ("LAST USED CHART — 5/7/2026, 12:47:10 AM") for non-current/non-legacy charts, instead of the old "CHART 3 — ..." numeric label. New _srfNameForId(id) helper resolves a chart id to its current display name (LEGACY CHART, CURRENT CHART, LAST USED CHART, LAST LAST USED CHART, LAST LAST LAST USED CHART) so preview titles always match the row labels. (9) The LOAD CHART confirm modal also reads names now. srfRequestLoadFromHistory injects a third line into the modal body that says "Replacing <current name> with <target name>." — replacing the previous "CHART 4 WITH CHART 3" numeric phrasing. The preview modal\'s "Load Chart" button is also re-routed through srfRequestLoadFromHistory so it goes through the named-confirm modal instead of the old window.confirm(\'CONFIRM REPLACING CHART...\') call. (10) VERSION STAMP. Bumped from B8.0 -> B8.1 throughout: title bar (GB-BizCalc - B8.1), version-badge, all PDF headers/footers, every save-as filename (every -B8.0.pdf -> -B8.1.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.1" news PDF footer string, internal VERSION constants updated to 'B8.1', AND the JSON export __version__ field now reads 'B8.1'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.1". NO data paths changed beyond the nine feature changes above. The B8.0, B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B8.0May 2026Eight changes on top of B7.9 plus the version bump. (1) HS CODE HISTORY drag mechanic was buggy — the panel would jump an inch southeast on every drag-start. The whole drag system is removed, replaced by a SELECT button that lives inside the right edge of the HTS / HS Code input. Clicking SELECT opens the HS CODE HISTORY panel anchored just above the button with a small connector arrow pointing back down to it. SELECT uses the FAVORITES yellow palette: light yellow base (#fff7c0 + #f59e0b border) flips to the rich dark yellow gradient (linear-gradient #fdd835 -> #ffeb3b) with black text on hover, matching the existing news-favorites-btn look. The input still rejects anything but digits and periods. (2) CHART FAVORITES system. Each row in CHART HISTORY now starts with a star icon: gray (\u2606) when not favorited, yellow (\u2605) when favorited. Clicking a gray star adds the chart to favorites; clicking a yellow star removes it. New FAVORITES button on the CHART HISTORY modal header (right side, light yellow palette matching SELECT) opens a new center-screen popup (srfFavModal) that lists every favorited chart, max 10. Persistence: localStorage[gbBizCalcShipChartFavorites_v1]. Inside the favorites popup each chart can be renamed via double-click on the name (55-char cap, Enter commits / Escape cancels), has its own PDF + LOAD CHART + Delete row buttons, and a yellow filled star to remove it from favorites. Removing a favorite (Delete or X-on-star) prompts a confirm and moves the entry into the Last Deleted Charts ring. (3) Last Deleted Charts ring buffer cap goes from 3 to 5 so favorites + manual deletes have more headroom before permanent loss. (4) LOAD CHART double-click confirm. Non-protected rows in CHART HISTORY (everything that\'s not LEGACY or CURRENT) now have a third action button between PDF and Delete: LOAD CHART. Double-clicking it opens a center-screen confirm modal (srfLoadModal) that reads "CONFIRM TO LOAD AND REPLACE EXISTING CHART" with YES / NO buttons. YES copies the selected chart\'s data into PACK_DATA, persists overrides, and pushes it as a new history entry — so the previously CURRENT chart cycles down to LAST USED CHART. The same flow is reused from the FAVORITES popup\'s LOAD CHART button (srfRequestLoadFromHistory). (5) Chart preview modal header gains a star + FAVORITES button on its right side (next to the close X). The star (#srfPrevFavStar) tracks the currently-previewed chart\'s favorite state and toggles it via srfPreviewToggleFav, syncing the per-row stars in the history list when re-rendered. The FAVORITES button opens the favorites popup directly from the preview. (6) UPDATE TARIFFS button now goes through a confirm popup. Clicking it shows a center-screen modal (gb-update-modal) saying "CONFIRM TO UPDATE TARIFFS" with YES / NO. Only YES kicks off the actual fetch. The original async function was renamed to _gbReallyUpdateTariffs and updateTariffs now just opens the popup so existing onclick="updateTariffs()" wiring continues to work. (7) UPDATE NEWS button gets the same treatment — clicking it shows a "CONFIRM TO UPDATE THE NEWS" popup, YES kicks off loadNews(true,{userInitiated:true}). Existing newsRefreshBtn onclick="userRefreshNews()" wiring continues to work. (8) VERSION STAMP. Bumped from B7.9 -> B8.0 throughout: title bar (GB-BizCalc - B8.0), version-badge, all PDF headers/footers, every save-as filename (every -B7.9.pdf -> -B8.0.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B8.0" news PDF footer string, internal VERSION constants updated to 'B8.0', AND the JSON export __version__ field now reads 'B8.0'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B8.0". NO data paths changed beyond the seven feature changes above. The B7.9, B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.9May 2026Five fixes on top of B7.8 plus the version bump. (1) Cost Per Piece — Manufacturer (USD) heading is now "Cost Per Piece — MFR (USD)". Single label change in the customer info card; the input id (mfrCostPerPiece) and downstream wiring are unchanged. (2) CHART HISTORY auto-cycling. The history list is now capped at 4 entries (CURRENT + 3 LAST USED slots) — every Edit-and-Lock cycle pushes a new snapshot then while(h.length > 4) h.shift() drops the oldest entry off the back. Names are positional now: the most-recent entry always renders as "CURRENT CHART" (no "CHART 2 (CURRENT CHART)" prefix anymore), and the previous entries — newest first — get "LAST USED CHART", "LAST LAST USED CHART", and "LAST LAST LAST USED CHART". As the cycle rotates the labels re-evaluate automatically: when a new chart pushes, the old CURRENT becomes LAST USED, old LAST USED becomes LAST LAST USED, etc. New helper _srfPositionalName(posFromCurrent) returns the correct label given a 0-based offset from the current row. The LEGACY CHART (B7.8 #2) still pins above this whole stack and is unchanged. (3) Mass-delete UI removed. The CHART HISTORY master bar (select-all checkbox + Delete Selected button + "Deleting more than 3 at once is permanent" hint) is gone, and so are the per-row checkboxes — none of it is needed once the cycle auto-caps at 4. _srfRenderHistory no longer emits any of that markup. The B7.6/B7.7 JS helpers (_srfHistMasterToggle, _srfHistDeleteSelected, _srfHistToggleSelect, _srfHistSelected) are kept as dead code so any external reference still resolves but they are no longer wired to any UI. (4) HS CODE HISTORY panel default position. The panel now pops up over the SHIPPING REFERENCE area (its top edge sits flush above the SHIPPING REFERENCE card and therefore above the HTS / HS Code field). htsShowPanel measures #shippingRef.getBoundingClientRect() when that card is visible (offsetParent !== null), and falls back to the B7.8 .field anchor if the SHIPPING REFERENCE card is hidden (no pack selected). Width still tracks the htsCode input. (5) HS CODE HISTORY panel is drag-and-droppable. Grab the header bar (the area between the "HS CODE HISTORY" title and the "+" button) and drag the panel wherever you want; release to drop. The .hts-hist-head bar gets cursor:grab + user-select:none, and a .hts-hist-head-dragging class flips to cursor:grabbing during the drag. Mouse-down inside a header button (Add, History Log, Close) short-circuits the drag so clicks still work normally. Position is stored in sessionStorage[gbBizCalcHtsPanelPos_v1], so closing the panel and reopening it brings it back to where you left it within the same session — but reloading the page (or closing the app) resets the panel back to the default anchor above SHIPPING REFERENCE. New helpers _htsLoadPanelPos / _htsSavePanelPos / _htsAttachDragHandlers. (6) VERSION STAMP. Bumped from B7.8 -> B7.9 throughout: title bar (GB-BizCalc - B7.9), version-badge, all PDF headers/footers, every save-as filename (every -B7.8.pdf -> -B7.9.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.9" news PDF footer string, internal VERSION constants updated to 'B7.9', AND the JSON export __version__ field now reads 'B7.9'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.9". NO data paths changed beyond the five fixes above. The B7.8, B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.8May 2026Six fixes on top of B7.7 plus the version bump. (1) CURRENT CHART is now permanently pinned at the top of CHART HISTORY (just under the LEGACY CHART) and is no longer deletable. The per-row checkbox is replaced by a spacer for the current row, the row-level Delete button is omitted, the preview-modal\'s Delete button is hidden when the row is current, srfDeleteFromHistory(id) refuses to delete the most-recent history id with an alert, and _srfHistDeleteSelected filters the protected ids out of the mass-delete batch before computing the >3-permanent flag so a master select-all + Delete Selected can never wipe the current chart. The render order in the active list is now: LEGACY (top) + CURRENT (next) + everything else (newest first). (2) LEGACY CHART. New permanent row pinned ABOVE the current chart in CHART HISTORY. It\'s an immutable one-time snapshot of PACK_DATA captured the first time the B7.8 build is opened and stored under localStorage[gbBizCalcShipChartLegacy_v1]. New helpers _srfLoadLegacy, _srfSaveLegacy, _srfEnsureLegacySnapshot. The IIFE that runs _srfApplyOverrides + _srfEnsureInitialSnapshot on script parse now also runs _srfEnsureLegacySnapshot. The legacy entry is identified by the SRF_LEGACY_ID virtual id (\'LEGACY_CHART\'); _srfFindEntry recognises it and returns the legacy slot data without going through the history list. The legacy row has its checkbox + rename + Delete buttons all suppressed. PDF still works (you can export the legacy snapshot any time). (3) HS CODE HISTORY panel anchor — now floats ABOVE the HTS/HS Code field group instead of below the input. htsShowPanel measures the field\'s parent .field rect (which contains the label heading), opens the panel + renders so it can read panel.getBoundingClientRect().height, then sets panel.style.top = scrollY + r.top - panelHeight - 6 so the panel sits flush above the "HTS / HS Code" heading. Width still tracks the htsCode input width (min 380px). (4) HTS/HS Code input is now digit + period only. The htsCode <input> gained oninput=\"this.value=this.value.replace(/[^0-9.]/g,'');\" plus inputmode="decimal" and pattern="[0-9.]*", so any letter, space, dash, slash, or other non-numeric character is stripped on every keystroke. The 15-char maxlength carries over. (5) HS CODE HISTORY panel header gained a green "+" button (.hts-hist-add-btn, #16a34a fill). New htsAddNewEntry(evt) prepends a fresh {id, code:\'\', name:\'\', ts} row to the active list, re-renders the panel, re-anchors it (since panel height likely changed), and focuses the new code input so the user can type immediately. If the user happens to be on the History Log view it flips back to the active view first. The CSS rule sits next to the existing .hts-hist-log-btn / .hts-code-input rules. (6) SHIPPING REFERENCE chart (FEU) 40 FT. Container column no longer overflows the gray ref-card frame. Root cause: .ref-grid used grid-template-columns:1fr 1fr 1fr which defaults to min-content sizing on each track — long values like 2,592,000 forced the FEU track wider than the card, pushing its right edge past the .ref-card padding. Fixed by switching to minmax(0,1fr) on every column track and adding min-width:0 + gap:8px to .ref-row plus flex:1 1 auto + overflow:hidden + text-overflow:ellipsis on .ref-row .ref-key (so the label can shrink instead of pushing the value off-track). The value spans/inputs get flex:0 0 auto + text-align:right so they hold their natural width inside the bounded column. (7) VERSION STAMP. Bumped from B7.7 -> B7.8 throughout: title bar (GB-BizCalc - B7.8), version-badge, all PDF headers/footers, every save-as filename (every -B7.7.pdf -> -B7.8.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.8" news PDF footer string, internal VERSION constants updated to 'B7.8', AND the JSON export __version__ field now reads 'B7.8'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.8". NO data paths changed beyond the six fixes above. The B7.7, B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.7May 2026Four fixes on top of B7.6 plus the version bump. (1) CHART HISTORY checkboxes — replaced the native macOS checkboxes (which had hard-to-see checkmarks) with custom-styled boxes via appearance:none. New CSS rules on .srf-hist-row-cb and .srf-hist-master-cb: 18px squared, 1.5px gray border, white background; on :hover the border flips red (#dc2626); on :checked a red dot fills the center via a ::after pseudo-element (10px circle, #dc2626 fill, centered with translate(-50%,-50%)). The user now gets a clear, unmistakable red dot whenever a row or the master checkbox is selected. The master checkbox also gained an onclick=\"event.stopPropagation()\" guard so clicking it doesn\'t bubble up to row handlers. (2) Mass-delete >3 charts is now PERMANENT. Added the phrase "Deleting more than 3 at once is permanent" as an italicized 10px hint label (.srf-hist-master-warn) that sits to the right of the Delete Selected button on the master bar. The master bar CSS gained flex-wrap:wrap so the hint reflows on narrow widths. _srfHistDeleteSelected now computes isPermanent = ids.length > 3 && !_srfDeletedView; when permanent, the selected entries skip the deleted ring buffer entirely (no dlist.unshift, no _srfSaveDeleted call) and are simply filtered out of the active list. 1-3 charts still get added to the restore log as before. The confirm dialog for >3 deletes also appends a NOTE warning the user that the entries will be permanently removed. (3) EDIT CHART non-editable cells — now correctly render as plain inline text. Root cause was CSS specificity tiebreak: both .ref-val-edit-computed (override) and .ref-val-edit (base) have equal specificity (single class), but .ref-val-edit-computed sat BEFORE .ref-val-edit in the stylesheet, so the base rule\'s border:1px solid #16a34a + background:#fff won the tiebreak. Fixed by adding !important to every override property on .ref-val-edit-computed: background:transparent !important, border:none !important, outline:none !important, color:var(--text) !important, cursor:default !important, padding:0 !important, box-shadow:none !important. The :focus pseudo also gains !important on outline/border/box-shadow. The four editable cells (Packs/MC, MC/Pallet, TEU pallets, FEU pallets) keep their green-bordered inputs; the six computed cells now show as plain bold text on the chart\'s default background. (4) HS CODE history panel polish. Removed the single-vs-double-click distinction (the 240ms timer was awkward) — clicking or focusing the htsCode input now always opens the panel. New _htsSeedFromInput() runs on init: if the active list is empty it pushes the current htsCode value (e.g. the default 8211.92.4060) along with the productName as the seed entry, so the panel never appears empty when the user first clicks into the field. Active rows now render with two side-by-side inline-editable inputs instead of plain spans: a .hts-code-input (130px, monospace bold, 15-char cap) and a .hts-name-input (flex:1, regular, 80-char cap, "Product name…" placeholder). New htsActiveUpdateField(id, field, value) helper writes the typed value straight into the storage list on every keystroke (slice-capped per field), and mirrors changes back into the htsCode + productName main inputs when the row matches the currently-selected code. No double-click confirm is required to type a name — the user just clicks into the cell and types. The body max-height is also bumped from 340px to 440px so ~10 rows are visible without scrolling. The B7.6 right-click-to-delete and HISTORY LOG view + restore-tooltip flows are unchanged. (5) VERSION STAMP. Bumped from B7.6 -> B7.7 throughout: title bar (GB-BizCalc - B7.7), version-badge, all PDF headers/footers, every save-as filename (every -B7.6.pdf -> -B7.7.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.7" news PDF footer string, internal VERSION constants updated to 'B7.7', AND the JSON export __version__ field now reads 'B7.7'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.7". NO data paths changed beyond the four fixes above. The B7.6, B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.6May 2026Three features on top of B7.5 plus the version bump. (1) EDIT CHART non-editable cells render as plain inline text. CSS rule .ref-val-edit-computed flipped from a grayed input (white box, green-outline border) to a transparent / borderless / paddingless cell. Black text on default background, font-weight:600 so it still reads like a value, cursor:default. Result: in edit mode the four editable inputs stand out clearly and the six computed cells stay legible without looking like inactive form fields. (2) CHART HISTORY panel adds per-row checkboxes + master select-all + Delete-Selected button + double-click-to-rename. New CSS classes .srf-hist-row-cb, .srf-hist-master-bar, .srf-hist-master-cb, .srf-hist-master-del, .srf-hist-row-name-input. Master bar renders above every list (active or deleted), even when empty, so the controls are always discoverable. _srfHistSelected tracks per-id selection state in memory; toggling a row updates the set and re-syncs the master checkbox. _srfHistMasterToggle selects-all or clears-all depending on its current state. _srfHistDeleteSelected gates on a window.confirm("CONFIRM DELETION OF N SELECTED CHART(S)") and either moves the selected entries into the deleted ring buffer (active list) or removes them permanently (when in HISTORY LOG view). _srfHistStartRename replaces the chart name span with a 55-char-cap input, blur or Enter commits the new name onto entry.name (Esc cancels). _srfHistResolveName prefers the user-edited name and falls back to "CHART N". Because the name is stored on the history entry itself, the value travels through every state — including the delete -> deleted-ring -> restore round trip — so a renamed chart stays renamed across all those transitions. (3) HS CODE history panel. Single-clicking the htsCode input opens a floating panel anchored just below the input, listing every saved entry as <code> <product name> rows with up to ~340px scrollable height (~10 rows visible). Double-clicking the input bypasses the panel via a 240ms click-distinguishing timer. The panel header has the title, a yellow HISTORY LOG button (FAVORITES yellow #fde68a), and an X close. Inside the active list: single-click a row to populate the htsCode + productName fields with that entry; right-click a row to delete it (window.confirm("CONFIRM TO DELETE HS CODE" + code + name)) — deleted entries move to the HISTORY LOG ring; double-click the code or name to confirm (window.confirm("CONFIRM YOU WANT TO EDIT HS CODE")) and edit in place (15-char cap on code, 80-char cap on name). In the HISTORY LOG view, right-click or double-click any row pops a small floating "ADD BACK TO LIST" tooltip near the cursor; clicking that tooltip restores the entry to the active list. Whenever the user types into htsCode and then either presses Enter or blurs, htsOnInputChange compares the new value to the previous and (if changed and not already in history) opens a center-screen popup "SAVE NEW HS CODE" with two paths: OVERWRITE the previously-active entry (keeps its name, just swaps the code) or ADD NEW (using the productName field as the default name, with an editable input). Storage: gbBizCalcHtsHistory_v1 (active, newest-first) and gbBizCalcHtsHistoryDeleted_v1 (log). Wired up via a DOMContentLoaded init that adds click + dblclick + blur + keydown handlers to #htsCode plus an outside-click dismiss for the panel and tooltip. The existing "Search HTS Codes" button + dropdown of external lookup links is unchanged. (4) VERSION STAMP. Bumped from B7.5 -> B7.6 throughout: title bar (GB-BizCalc - B7.6), version-badge, all PDF headers/footers, every save-as filename (every -B7.5.pdf -> -B7.6.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.6" news PDF footer string, internal VERSION constants updated to 'B7.6', AND the JSON export __version__ field now reads 'B7.6'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.6". NO data paths changed beyond the three features above. The B7.5, B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.5May 2026Four changes on top of B7.4 plus the version bump. (1) PRE-PAID SHIP-CALC SUB PROFILE menu drops Profile 4 + Profile 5. SHIP_SUB_IDS goes from ['1','2','3','4','5'] to ['1','2','3'] so the Sub Profile dropdown now lists only Profile MAIN / A / B (matching the ROI-Calc B6.6 restructure). The shipCalcBuild populator also auto-resets the priorSub fallback to '1' if the previously-saved value pointed at a stale 4 or 5. (2) EDIT CHART button shows red "LOCK CHART" while editing. CSS adds a .active state on .ship-ref-edit-btn that flips background to #dc2626 (red-600) with white text + #991b1b border, plus a darker hover. srfConfirmYes (entering edit mode) sets btn.textContent = 'LOCK CHART' and adds the .active class. onShipRefEditBtnClick's lock branch reverses both, restoring 'EDIT CHART' as the label and dropping the .active class. The :hover green flash on the gray default state is preserved so an out-of-edit-mode cursor over the button still hints it's clickable. (3) CLIPBOARD label persistence fix. The B7.0 #3 / B7.0 raised the auto-label slice cap to 50 chars, but roiClipboardsLoadAll was still clamping back down to 20 chars on every page load via slot.name.slice(0, 20). Bumped the load-side slice to 50 chars too so labels like "Ace Hardware (HQ) - MAIN" survive across app close + reopen without losing the trailing profile letter. The save-side cap + rename-input maxLength + commit-time slice were already at 50 since B7.0, so the round-trip is now consistent. (4) SHIPPING REFERENCE chart auto-computed cells. Editable cells while in edit mode are limited to four: Packs per Master Carton, Master Cartons per Pallet, (TEU) Pallets per Container, and (FEU) Pallets per Container. Every other numeric cell — Packs per Pallet, Individual pcs per Pallet, (TEU) Packs per Container, (TEU) Individual pcs per Container, (FEU) Packs per Container, (FEU) Individual pcs per Container — is now rendered as a read-only grayed input (.ref-val-edit-computed CSS class) with cursor:not-allowed and a hover-tooltip explaining it's computed automatically. The four formulas: Packs per Pallet = packsPerMC \u00d7 mcPerPallet; Individual pcs per Pallet = packsPerPallet \u00d7 packSize (10 or 40); TEU Packs per Container = teuPallets \u00d7 packsPerPallet; TEU Individual pcs per Container = teuPacks \u00d7 packSize; FEU mirrors the TEU pattern using feuPallets. New helper _srfRecomputeDependents(packSize) writes the freshly-computed values back into PACK_DATA so every downstream proxy (ROI_PACKS_PER_MC, ROI_MC_PER_PALLET, ROI_PALLET_BLADES) sees them on the next read. New helper _srfRefreshComputedInputs(packSize) updates each computed input's .value live so the on-screen chart reflects the new totals as the user types into the editable cells (no full re-render needed, so cursor position in the edit field is preserved). srfOnEditValueInput now early-returns when called against a .ref-val-edit-computed input (defensive guard for synthetic events) and invokes both helpers after every successful edit to an editable cell. End-state for the user's test: edit Packs per Master Carton from 120 to 100 \u2192 Packs per Pallet auto-updates from 5,400 to 5,000 (= 100 \u00d7 50 if the user also bumped MC/Pallet to 50), Individual pcs per Pallet auto-updates to 50,000 (= 5,000 \u00d7 10), and the (TEU)/(FEU) Packs and Individual pcs per Container values follow suit. Locking the chart still pushes the snapshot to history + persists overrides + triggers _srfRefreshDependents() so the ROI-Calc / Tariff-Calc / Ship-Calc readouts pick up the new chain. (5) VERSION STAMP. Bumped from B7.4 -> B7.5 throughout: title bar (GB-BizCalc - B7.5), version-badge, all PDF headers/footers, every save-as filename (every -B7.4.pdf -> -B7.5.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.5" news PDF footer string, internal VERSION constants updated to 'B7.5', AND the JSON export __version__ field now reads 'B7.5'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.5". NO data paths changed beyond the four fixes above. The B7.4, B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.4May 2026One change on top of B7.3 plus the version bump. (1) LANDED / PALLET FORMULA NOW USES THE packsPerMC \u00d7 mcPerPallet \u00d7 pack CHAIN. The B7.2-B7.3 changes wired live proxies to the multipliers and the headings updated correctly, but the dollar value under LANDED / PALLET still used the old formula `landedPc \u00d7 bladesPerPallet` (where bladesPerPallet = PACK_DATA[pack].pcsPerPallet). Because pcsPerPallet is a separately-stored constant in PACK_DATA that the chart-edit flow doesn't recompute, edits to "Packs per Master Carton" or "Master Cartons per Pallet" never flowed into the displayed pallet dollar value. Per user spec the pallet value should equal the LANDED / N PK MC value times the MC-per-pallet multiplier — i.e. landedPc \u00d7 pack \u00d7 packsPerMC \u00d7 mcPerPallet. Updated three call sites: (a) roiUpdateLandedReadout in the live ROI-Calc card readouts. New intermediate variables `landedMc = landedPc \u00d7 roiSubtab \u00d7 packsPerMC` and `landedPallet = landedMc \u00d7 mcPerPallet` make the chain explicit. (b) Per-card ROI PDF exporter — pallet row label changed from "Landed / Pallet (N blades)" (where N was static pcsPerPallet) to "Landed / Pallet (xN MC)" matching the on-screen sub-label introduced in B7.2 #1, and the value uses the same chain. (c) ALL COST BASIS combined ROI PDF exporter — same change as (b). End-state for the user's test: with $0.3807 / piece, packsPerMC=100, and mcPerPallet=50, LANDED / 10 PK MC (x100) now shows $380.70 = 0.3807 \u00d7 10 \u00d7 100, and LANDED / PALLET (x50 MC) now shows $19,035.00 = $380.70 \u00d7 50. Edits to ANY of these three multipliers in the SHIPPING REFERENCE chart immediately propagate to all four ROI-Calc landed cells (PIECE / PACK / MC / PALLET), the GB distributor + MSRP margin readouts that derive from them, and both ROI PDF exports. The ROI_PALLET_BLADES proxy from B7.2 is still defined for backward-compat with any other call site that may read it, but is no longer the source of truth for the LANDED / PALLET dollar value. (2) VERSION STAMP. Bumped from B7.3 -> B7.4 throughout: title bar (GB-BizCalc - B7.4), version-badge, all PDF headers/footers, every save-as filename (every -B7.3.pdf -> -B7.4.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.4" news PDF footer string, internal VERSION constants updated to 'B7.4', AND the JSON export __version__ field now reads 'B7.4'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.4". NO data paths changed beyond the formula updates above. The B7.3, B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.3May 2026One change on top of B7.2 plus the version bump. (1) ROI-CALC + PRE-PAID SHIP-CALC NOW AUTO-REFRESH AFTER A SHIPPING REFERENCE CHART EDIT. The B7.2 proxies wired live PACK_DATA values into the calculation paths so the heading multipliers like "(x100)" and "(x50 MC)" updated correctly, BUT the dollar values rendered into the roi-landed-mc-* / roi-landed-pallet-* DOM cells stayed at the old amounts because nothing called roiUpdateLandedReadout / roiRecalc again after the chart was locked. New helper _srfRefreshDependents() invokes (in order) roiRebuild() (clears and re-renders every ROI card so heading templates AND value cells re-evaluate against live PACK_DATA), shipCalcRefresh() (re-runs Pre-Paid Ship-Calc's grid build), updateShippingBadges() (refreshes the Tariff-Calc Pallets / TEU / FEU button hint badges), and recalcPieces() (re-runs the Tariff-Calc auto-fill quantity from the active pack/ship mode). Each call is wrapped in try/catch so a missing function early in page lifecycle doesn't abort the rest. The helper is invoked from two locations: (a) onShipRefEditBtnClick's lock branch, immediately after persisting overrides + pushing a history snapshot + re-rendering the SHIPPING REFERENCE chart in read-only mode \u2014 so the user's edits flow through the moment they click EDIT CHART a second time. (b) srfLoadFromPreview, immediately after deep-copying a history snapshot into PACK_DATA + persisting overrides + pushing a fresh history entry + re-rendering the live chart \u2014 so loading an older chart from the CHART HISTORY panel instantly recomputes every dependent value. End-state: the user's test case (changing "Packs per Master Carton" from 120 to 100 and "Master Cartons per Pallet" from 45 to 50) now correctly recalculates "LANDED / 10 PK MC (x100)" from $456.84 down to $380.70 (= $0.3807 \u00d7 10 \u00d7 100) and "LANDED / PALLET (x50 MC)" from $20,557.80 down to $19,035.00 (= $380.70 \u00d7 50). Tariff-Calc landed math (LANDED / PIECE / PACK) is unchanged because it never depended on the MC or pallet multipliers. (2) VERSION STAMP. Bumped from B7.2 -> B7.3 throughout: title bar (GB-BizCalc - B7.3), version-badge, all PDF headers/footers, every save-as filename (every -B7.2.pdf -> -B7.3.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.3" news PDF footer string, internal VERSION constants updated to 'B7.3', AND the JSON export __version__ field now reads 'B7.3'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.3". NO data paths changed beyond the helper + two call sites above. The B7.2, B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.2May 2026Two changes on top of B7.1 plus the version bump. (1) SHIPPING REFERENCE EDITS NOW PROPAGATE LIVE TO ROI-CALC + PRE-PAID SHIP-CALC. The previously-hardcoded constants ROI_PACKS_PER_MC ({10:120, 40:24}) and ROI_PALLET_BLADES ({10:54000, 40:51840}) are converted to Proxy-backed objects whose property reads pull live from PACK_DATA[pack].packsPerMC and PACK_DATA[pack].pcsPerPallet on every access — so every existing call site like ROI_PACKS_PER_MC[10] automatically reflects the user's edits made through the SHIPPING REFERENCE EDIT CHART flow without any other code changes. New sibling proxy ROI_MC_PER_PALLET reads PACK_DATA[pack].mcPerPallet so the LANDED / PALLET label can show its live "(x{N} MC)" sub-multiplier. The ROI_SKUS.mcMult field is converted from a hardcoded number (120/24) into an Object.defineProperty getter that resolves to the live ROI_PACKS_PER_MC[pack] for MC SKUs and 1 for single-pack SKUs — so every call site that reads sku.mcMult also picks up the live value (used by ROI-Calc landed math, the Pre-Paid Ship-Calc per-MC math, and both ROI PDF exporters). End-state: when the user changes "Packs per Master Carton" in the SHIPPING REFERENCE chart from 120 to 100, the LANDED / 10 PK MC heading in every 10 Pack and 10 Pack MC ROI panel switches from "(x120)" to "(x100)" AND the dollar value drops to landedPc \u00d7 10 \u00d7 100 (instead of 120). Same chain for the 40 PACK side, and the LANDED / PALLET dollar value updates whenever Master Cartons per Pallet or pcs/Pallet changes. The (xN MC) sub-label on LANDED / PALLET is new this release: was "Landed / Pallet", now "Landed / Pallet (x{N} MC)" where N is the live PACK_DATA[pack].mcPerPallet (45 / 54 by default). Pre-Paid Ship-Calc rows automatically inherit the live values because they read sku.mcMult and ROI_PACKS_PER_MC[pack] through the same proxies. The MFR-CostCalc PACK constant remains hardcoded per spec — only the "1 PACK" and "1 BLADE / PC" panels there are exempt from chart-driven updates and the rest of MFR-CostCalc was not in scope for this pass. (2) SHIPPING REFERENCE PDF HEADING TAGS CHART-HISTORY DOWNLOADS. exportShipRefChartsPDF now accepts an optional contextLabel parameter; the live PDF button passes nothing (heading reads "GB-BizCalc \u2014 SHIPPING REFERENCE" as before), while srfDownloadHistoryPDF passes "(CHART HISTORY - CHART N)" for normal history rows or "(CHART HISTORY - CHART N (RESTORED))" when the entry was restored from the deleted ring buffer. Restored entries are now flagged with _restored:true inside srfRestoreDeleted so the marker survives across subsequent loads, deletions, and PDF downloads. End-state: a PDF emitted by clicking the orange PDF button on a CHART 2 history row shows "GB-BizCalc \u2014 SHIPPING REFERENCE (CHART HISTORY - CHART 2)" in its top-left header bar; if that CHART 2 row was restored from the deleted view its PDF reads "(CHART HISTORY - CHART 2 (RESTORED))". PDFs downloaded directly from the live chart still show no parenthetical at all. (3) VERSION STAMP. Bumped from B7.1 -> B7.2 throughout: title bar (GB-BizCalc - B7.2), version-badge, all PDF headers/footers, every save-as filename (every -B7.1.pdf -> -B7.2.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.2" news PDF footer string, internal VERSION constants updated to 'B7.2', AND the JSON export __version__ field now reads 'B7.2'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.2". NO data paths changed beyond the proxy + getter swaps above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B7.1, B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.1May 2026Three changes on top of B7.0 plus the version bump. (1) UNDO/REDO REMOVED. The B6.9/B7.0 Cmd/Ctrl+Z + Cmd/Ctrl+Y subsystem (localStorage.setItem hook + debounced snapshot pushes + stacks at __gbUndoStack_v1 / __gbRedoStack_v1 + capture-phase keydown listener on document and window) is fully stripped per user request. The single legacy " - HQ" \u2192 " (HQ)" migration that lived inside that IIFE is kept as a small standalone IIFE. (2) SHIPPING REFERENCE: EDIT CHART + CHART HISTORY. New "EDIT CHART" button sits to the LEFT of the existing orange PDF button on the SHIPPING REFERENCE chart title row. Default state is gray (#9ca3af bg / #6b7280 border / white text); :hover and .active flip both background and border to green (#16a34a / #15803d). Click opens a center-screen confirm popup styled as a standalone modal with two-line title rendered in 14px uppercase 800-weight (vertically spaced 8px apart): "!! ARE YOU SURE YOU WANT TO UPDATE" on the first line and "THE SHIPPING REFERENCE CHART? !!" on the second. Three buttons in the popup footer: YES (left), NO (middle), SEE HISTORY (right). YES + NO are gray default with blue #0a84ff hover; SEE HISTORY uses the FAVORITES yellow (#fde68a default \u2192 #fcd34d hover). The popup also has a standard X close in the top-right and dismisses on backdrop click. YES enters edit mode: every numeric value cell in the currently-displayed chart becomes a text input with auto-comma formatting on every keystroke (raw digits parsed, Number.toLocaleString() for display) wired through srfOnEditValueInput which writes into PACK_DATA[packSize][key] live so any in-app calculation that reads PACK_DATA picks up the new value immediately. The EDIT CHART button stays green while editing. Clicking it again locks: persists the live PACK_DATA into a new gbBizCalcShipChartOverrides_v1 entry so edits survive page reloads, and pushes a new snapshot ({ id, createdAt, data: { 10:{...}, 40:{...} } }) onto the gbBizCalcShipChartHistory_v1 array. On every page load, _srfApplyOverrides() merges the persisted overrides back into PACK_DATA so calculations downstream (ROI-Calc, Pre-Paid Ship-Calc) read the user-edited values, and _srfEnsureInitialSnapshot() guarantees CHART 1 (the as-shipped defaults) is in history before any edits. SEE HISTORY in the confirm popup opens a separate Chart History modal listing every saved snapshot top-down (CHART 1, CHART 2, ...) each with the parsed createdAt rendered as a centered grayed timestamp underneath, an orange PDF button, and a red DELETE button. The latest entry is highlighted in green and tagged "(current)". Double-clicking any row opens a Chart Preview sub-modal that renders both the 10 PACK and 40 PACK snapshots in a compact three-column layout exactly mirroring the live chart, with footer buttons Cancel + PDF + Delete + Load Chart (Load is hidden when previewing the current chart). Clicking Load opens a center-screen confirm "CONFIRM REPLACING CHART X WITH CHART Y"; on OK we deep-copy the target snapshot's data into PACK_DATA + persist as overrides + push a fresh history entry, then re-render the live chart. Clicking Delete opens a center-screen confirm "CONFIRM DELETION OF CHART N"; on OK the snapshot is moved to a deleted-charts ring buffer (gbBizCalcShipChartDeleted_v1, FIFO max 3 entries) so it can be recovered. The header of the Chart History modal includes a small blue underlined "Last Deleted Charts" link that toggles the body to show only the deleted entries with PDF + Restore actions. Restore puts the entry back into history at its original index when possible. Each PDF action (in the list, in the preview, and the existing top-row PDF) temporarily swaps PACK_DATA to the snapshot\u2019s data, runs the existing exportShipRefChartsPDF() pipeline, then restores PACK_DATA \u2014 so every chart-history PDF looks identical to the live PDF download. All non-confirm popups (the EDIT CHART confirm, the Chart History modal, the Chart Preview sub-modal) have an X in the top-right + dismiss on backdrop click + dismiss on Esc. (3) HEADING TEXT "Master Cartons per Pallet (MC)" \u2192 "Master Cartons per Pallet" (drop the parenthetical " (MC)" suffix per spec). (4) VERSION STAMP. Bumped from B7.0 -> B7.1 throughout: title bar (GB-BizCalc - B7.1), version-badge, all PDF headers/footers, every save-as filename (every -B7.0.pdf -> -B7.1.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.1" news PDF footer string, internal VERSION constants updated to 'B7.1', AND the JSON export __version__ field now reads 'B7.1'. The CSV download _ suffix introduced in B7.0 #4 automatically advances to "_B7.1" via the live VERSION constant. NO data paths changed: every prior feature from B5.5 through B7.0 is preserved byte-for-byte except for the three surgical edits above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B7.0, B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B7.0May 2026Four fixes on top of B6.9 plus the version bump. (1) SHIPPING REFERENCE PDF COLUMN OVERLAP. The B6.9 #5 PDF export rendered each column at ~159pt wide with 10pt body text, which wasn't enough room for long row labels like "Master Cartons per Pallet (MC)" — the label spilled past the column boundary into the value text of the next column. Fixed by reserving a 64pt right-side value column per column box, dropping the body label font from 10pt to 9pt, shrinking the inner padding from 8 to 6, and wrapping multi-word labels onto a 2nd line via doc.splitTextToSize using the reserved label-only width (colW - padX*2 - valueColW). The right-aligned value still sits at colW - padX inside its column, so the value column is rigid and labels can't invade it. (2) UNDO / REDO NOT WORKING FIX. The B6.9 #2 implementation overrode localStorage.setItem directly (`localStorage.setItem = function...`), which Safari sometimes silently ignores because Storage instances have unusual property semantics. Switched to overriding Storage.prototype.setItem instead — the prototype-level hook is honored on Chrome, Safari, and Firefox. Inside the override `this === localStorage` checks that the call is on our localStorage and not a sessionStorage or other Storage instance. Also registered the keydown listener with capture phase (3rd arg true) on BOTH document AND window, so the event is intercepted before any element-level handler can stopPropagation() — Safari sometimes routes Cmd/Ctrl+Z to the focused input first, and capture-phase guarantees we see it. The handler also calls stopPropagation() in addition to preventDefault() so no downstream listener can react to the same press. Cmd+Z, Cmd+Shift+Z, and Cmd+Y all work; the Ctrl-prefixed equivalents work on Windows / Linux. (3) CLIPBOARD LABEL TRUNCATION FIX. The B6.9 auto-label code hard-capped the slot name at 20 characters via .slice(0, 20). For a card named "Ace Hardware (HQ)" (17 chars) plus " - " (3 chars) plus the profile letter, we hit exactly 20 chars and the profile letter was sliced off. Raised the slice cap to 50 characters in three places: the auto-label on drop-onto-clipboard, the rename input's maxLength, and the rename input's commit-time slice. CSS text-overflow:ellipsis on the .roi-clipboard pill still handles visual truncation when the label is wider than the slot. Per user request, the slot width is also reverted from B6.9's 210px back to B6.8's 180px — the user said "The CLIPBOARD buttons can go back to the width they were in B6.8 as that width will fit the text I am asking for just fine". Net result: a card named "Ace Hardware (HQ)" dragged to a clipboard slot now shows "Ace Hardware (HQ) - M\u2026" for the MAIN profile and "Ace Hardware (HQ) - A" / "Ace Hardware (HQ) - B" for A and B (the leading profile letter is always visible). (4) CSV DOWNLOAD FILENAME VERSION SUFFIX. CSV exports now end in `_` so e.g. "Ace_Hardware_HQ_SHOPIFY_PROFILE.csv" becomes "Ace_Hardware_HQ_SHOPIFY_PROFILE_B7.0.csv". Mirrors the version-stamped naming convention used by every PDF download in the app. The suffix uses the live VERSION constant so future bumps automatically advance the filename. (5) VERSION STAMP. Bumped from B6.9 -> B7.0 throughout: title bar (GB-BizCalc - B7.0), version-badge, all PDF headers/footers, every save-as filename (every -B6.9.pdf -> -B7.0.pdf, including GB-BizCalc-SHIPPING-REFERENCE-10-and-40-PACK-B7.0.pdf and the new CSV _B7.0 suffix), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B7.0" news PDF footer string, internal VERSION constants updated to 'B7.0', AND the JSON export __version__ field now reads 'B7.0'. NO data paths changed: every prior feature from B5.5 through B6.9 is preserved byte-for-byte except for the four surgical edits above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.9, B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.9May 2026Six changes on top of B6.8 plus the version bump. (1) CLIPBOARD BUTTONS WIDER. The four .roi-clipboard slots grow from 180px to 210px (+30px, ~5 character widths at the 10px font) so dropped customer-card-name labels show approximately 4 more characters before the text-overflow ellipsis kicks in. width / min-width / max-width all updated together so the slots stay rigid. (2) APP-WIDE Cmd/Ctrl+Z UNDO + Cmd/Ctrl+Y REDO. Up to 5 levels each. New IIFE hooks localStorage.setItem so every persistent state change captures a before-snapshot of all gbBizCalc* and tc_* keys; snapshot pushes are debounced 700ms so a burst of typing creates one undo entry rather than one per keystroke. Cmd/Ctrl+Z pops the undo stack, pushes current state to the redo stack, applies the snapshot, and reloads the page so every UI surface (Customer Cards, Customer Info panel, news, tariff results, pack panels, ROI cards, etc.) re-hydrates from the restored state. Cmd/Ctrl+Y mirrors. Cmd/Ctrl+Shift+Z is also wired as redo (macOS convention). Stacks persist across reloads at __gbUndoStack_v1 / __gbRedoStack_v1 (the leading underscores exclude them from the snapshot capture). (3) CSV FILENAME SIMPLIFIED. The CSV export filename drops the "cust-info-" prefix: was "cust-info-Ace_HRDWR_-_HQ-SHOPIFY_PROFILE.csv", now "Ace_HRDWR_-_HQ-SHOPIFY_PROFILE.csv" (slash format: <company>_<profile>.csv). (4) HQ SUFFIX CHANGED " - HQ" \u2192 " (HQ)". When the HQ checkbox in a CUSTOMER INFO panel is on, the companyName + Customer Card .name now end with " (HQ)" instead of " - HQ". The 3 helpers (_roiCInfoEndsWithHq, _roiCInfoStripHq, _roiCInfoApplyHq) are updated; _roiCInfoEndsWithLegacyHq is added so legacy data still hydrates correctly. _roiCInfoNormalize migrates legacy " - HQ" suffix on companyName to " (HQ)" on first read. The undo/redo IIFE also runs a one-time migration on roiCustomerCards on load to rewrite slot labels with the legacy suffix. (5) SHIPPING REFERENCE PDF BUTTON. Small orange "PDF" button now sits on the right of the SHIPPING REFERENCE chart title row. Clicking either chart's PDF button (10 PACK or 40 PACK) downloads a single 1-page PDF showing BOTH charts stacked. The PDF has a black header bar with title + version + timestamp, then the 10 PACK CONFIG chart at ~80% page width (3 columns: MC's/Pallets/Packs/PCS, TEU 20FT, FEU 40FT), then a thin gray divider with vertical white space above and below it, then the 40 PACK CONFIG chart in the same layout. Text size is ~12px header / 10px body so it's easy to read at print resolution. Filename: GB-BizCalc-SHIPPING-REFERENCE-10-and-40-PACK-B6.9.pdf. (6) VERSION STAMP. Bumped from B6.8 -> B6.9 throughout: title bar (GB-BizCalc - B6.9), version-badge, all PDF headers/footers, every save-as filename (every -B6.8.pdf -> -B6.9.pdf, including the new GB-BizCalc-SHIPPING-REFERENCE-10-and-40-PACK-B6.9.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.9" news PDF footer string, internal VERSION constants updated to 'B6.9', AND the JSON export __version__ field now reads 'B6.9'. NO data paths changed: every prior feature from B5.5 through B6.8 is preserved byte-for-byte except for the five surgical edits above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.8, B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.8May 2026Two changes on top of B6.7 plus the version bump. (1) CLIPBOARD BUTTONS WIDER. The four .roi-clipboard slot buttons in the Customer Cards bar grow from 156px to 180px (+24px, ~4 character widths at 10px font) so dropped customer-card-name labels render with more room before the text-overflow ellipsis kicks in. width / min-width / max-width all updated in lockstep so the slots stay rigid (no flex collapse). (2) CSV EXPORT FROM CUSTOMER INFO. New green "CSV" button (#16a34a background, white font) sits between the Notes and PDF buttons in the Company Name row of every CUSTOMER INFO panel. Click opens a sub-modal "EXPORT TO CSV" over the panel (z-index 2000) with three sections: (a) Three CSV PROFILE buttons at the top — defaults "CSV PROFILE 1", "CSV PROFILE 2", "CSV PROFILE 3". The active profile is highlighted in green. Below the profile bar sits an editable name input (maxlength 40) and a padlock toggle to its right. Padlock locked = name input is disabled (read-only); padlock unlocked = name input accepts edits, autosaved on every keystroke. The user can rename a profile to e.g. "Shopify" or "GoDaddy" once they unlock it, then re-lock to protect the name. A "Save to <profile-name>" button to the right of the padlock writes the current checkbox state into that profile (briefly flashes "Saved \u2713" as confirmation). (b) A scrollable checkbox list of every data heading in the panel — Company Name, Company URL, HQ, every Company Address subfield, every Shipping Address subfield, Company Notes, then per-contact: Title, Rep flag, Rep Company / Name, Rep Co. URL, First/Last Name, both Email slots, all four Phone slots (with their Type and Country sub-fields exposed as separate columns), Linked-In, Location, every Location Address subfield, Contact Notes. Each checkbox uses the green-dot-inside style introduced for the HQ / Shipping checkboxes in B6.0 #5/#6 (appearance:none + a 9px filled green dot when checked). When the underlying data is empty for that field — no value in the customer record for company-level fields, no value in any contact for contact-level fields — the row is rendered with .disabled class (gray text, opacity .55, cursor:not-allowed) AND the checkbox itself is disabled so it cannot be toggled. As soon as the user types something into a field in the panel, the corresponding CSV checkbox becomes active on next open. (c) A footer with Cancel and Download CSV buttons. Download reads the active profile's pending checkbox selections, filters out any disabled (no-data) selections, builds a CSV with one header row of selected field labels followed by one data row per contact (so a customer with 3 contacts gets 3 data rows, with company-level fields repeated on each row). If only company-level fields are selected, the CSV has exactly one data row. Values are properly quoted (commas, quotes, and newlines escaped via doubled quotes) and the file is emitted with a UTF-8 BOM so Excel respects non-ASCII characters. Filename: cust-info-<company>-<profile-name>.csv. Profiles persist to localStorage at gbBizCalcCsvProfiles_v1 with shape { 1: { name, locked, selections[] }, 2: {...}, 3: {...} } and reload across sessions. Esc closes the modal first (before the customer info panel itself), matching the precedence used by Notes / PDF / Country sub-modals. (3) VERSION STAMP. Bumped from B6.7 -> B6.8 throughout: title bar (GB-BizCalc - B6.8), version-badge, all PDF headers/footers, every save-as filename (every -B6.7.pdf -> -B6.8.pdf, including cust-info-...-B6.8.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.8" news PDF footer string, internal VERSION constants updated to 'B6.8', AND the JSON export __version__ field now reads 'B6.8'. NO data paths changed: every prior feature from B5.5 through B6.7 is preserved byte-for-byte except for the clipboard width (CSS), the new CSV button render line, the new CSV modal DOM, the new CSV JS module appended before the Esc-key handler, and the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.7, B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.7May 2026One change on top of B6.6 plus the version bump. (1) MODERNIZE CLEAR PROFILE CONFIRM + BLOCK WHEN LOCKED. Right-clicking a profile (MAIN / A / B) and clicking the CLEAR PROFILE row used to flip the floating menu into a second-step follow-up reading "Click to Confirm Clear Profile X" anchored above the cursor. The user has to click that floating button to actually clear. Replaced with the center-screen native window.confirm("CONFIRM CLEAR PROFILE X") pattern used everywhere else in the app (B5.5 #1 RESET confirm, B5.8 #1 CONTACT DELETION confirm, B5.6 etc.). On OK roiClearSubProfile(cardId, subId) wipes the per-Profile snapshot in the same way as before; on Cancel nothing changes. Additionally, when the right-clicked profile is padlocked (roiGetSubLocked returns true), the Clear Profile action no longer proceeds — clicking the row hides the menu and opens window.alert("UNLOCK PROFILE TO USE CLEAR FEATURE") instead. The user has to right-click \u2192 UNLOCK PROFILE first before the clear path is reachable. The legacy 'confirmClear' phase that the prior floating-button follow-up routed to is left as a defensive no-op (so any cached state from a B6.6-or-earlier menu interaction lands safely without throwing). All other right-click options on the profile menu (color picker squares + RESET COLOR + LOCK/UNLOCK PROFILE) are unchanged. (2) VERSION STAMP. Bumped from B6.6 -> B6.7 throughout: title bar (GB-BizCalc - B6.7), version-badge, all PDF headers/footers, every save-as filename (every -B6.6.pdf -> -B6.7.pdf, including cust-info-...-B6.7.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.7" news PDF footer string, internal VERSION constants updated to 'B6.7', AND the JSON export __version__ field now reads 'B6.7'. NO data paths changed: every prior feature from B5.5 through B6.6 is preserved byte-for-byte except for the two-branch clear-profile flow above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.6, B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.6May 2026Two changes on top of B6.5 plus the version bump. (1) CALCULATOR PASTE ENTIRELY REMOVED. The user reported the paste path is not workable on Safari/file:// (every variant we tried — direct readText, hidden-textarea native paste, execCommand('paste'), prompt() dialog — either surfaced Safari's clipboard-permission tooltip or failed silently). Decision: drop calculator paste entirely and keep only Copy. Removed from the popup script: the Paste button DOM row in the right-click ctx menu; the hidden, always-focused textarea + its paste event listener + the refocus listeners on mousedown / mouseup / click / window focus / textarea blur; the pasteFromClipboardToCalc() helper (navigator.clipboard.readText path); the showPasteHint() toast helper + its _gbCalcPasteHintEl / _gbCalcPasteHintTimer state vars; the _gbCalcPromptPaste() helper + its long comment header; the ctxPasteBtn click wiring; and the Cmd/Ctrl+V branch of the keydown handler. Removed from the main page: the gbCalcSetFromText(text) function + its comment header block. What stays: the Copy menu row (gbCalcCtxCopy), copyDisplayToClipboard() (uses navigator.clipboard.writeText which is silent on every browser), the Cmd/Ctrl+C branch of the keydown handler, the ctxCopyBtn click wiring, the contextmenu handler that shows the now-Copy-only menu, all calculator math + state + render code. End-state: right-click shows just "Copy", Cmd/Ctrl+C copies, no paste path anywhere — no more permission tooltips, no more silent paste failures. The user can copy a number out of the calc to use elsewhere; entering values into the calc is keyboard / button tap only. (2) PROFILES RESTRUCTURED TO MAIN / A / B (was A / B / C / D / E). Customer Card sub-profiles drop from 5 buttons to 3 with a renamed leading button. ROI_SUB_LETTERS goes from { '1':'A', '2':'B', '3':'C', '4':'D', '5':'E' } to { '1':'MAIN', '2':'A', '3':'B' } so the existing data for sub IDs 1, 2 and 3 is preserved exactly (the user's prior "A" data is now displayed under "MAIN", prior "B" data is now under the new "A", prior "C" data is now under the new "B"). ROI_SUB_IDS goes from ['1', '2', '3', '4', '5'] to ['1', '2', '3'] so D and E never render in any UI surface. Their localStorage entries (under sub IDs '4' and '5') are left on disk untouched — they're simply never read by any active code path. Visual layout: a new CSS rule .roi-card-sub-btn[data-sub-id="1"]{flex:3 1 0} makes the MAIN button take 3x the regular flex space inside the existing .roi-customer-slot-subs flex container, so MAIN visually fills the area that A+B+C used to occupy. The new A (sub id 2) sits in the slot prior D occupied; the new B (sub id 3) sits in the slot prior E occupied. Total bar width is unchanged. font-size is nudged down to 9.5px on the MAIN button so the longer label fits comfortably. Drag-drop (roiSubDrop / roiSubDragStart / roiSubDragEnd), padlock + lock-state UI (roiGetSubLocked, lock SVG), right-click context menus (roiShowSubProfileCtxMenu — color picker, lock toggle, duplicate, clear), the has-data dot, custom-color rendering, and B5.5 #2 swap-confirm all keep working without code changes because they iterate ROI_SUB_IDS and look up the display label via subIdToLetter(sid) which now returns 'MAIN' / 'A' / 'B' for IDs 1, 2, 3 respectively. (3) VERSION STAMP. Bumped from B6.5 -> B6.6 throughout: title bar (GB-BizCalc - B6.6), version-badge, all PDF headers/footers, every save-as filename (every -B6.5.pdf -> -B6.6.pdf, including cust-info-...-B6.6.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.6" news PDF footer string, internal VERSION constants updated to 'B6.6', AND the JSON export __version__ field now reads 'B6.6'. NO data paths changed: every prior feature from B5.5 through B6.5 is preserved byte-for-byte except for the calculator paste subsystem (entirely removed), the ROI_SUB_LETTERS / ROI_SUB_IDS constants, the new MAIN-button CSS rule, and the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.5, B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.5May 2026One change on top of B6.4 plus the version bump. (1) RELIABLE CALCULATOR PASTE VIA prompt() DIALOG. The B6.3 + B6.4 paste flow tried hidden-textarea native-paste plus execCommand('paste') plus a navigator.clipboard.readText() fallback. On Safari with file:// URLs every one of those paths either failed silently or surfaced the OS clipboard-permission tooltip — the user perceived the permission tooltip as a duplicate "Paste" menu item, and the silent-failure cases meant pasted numbers never reached the calculator state, so pressing = / + / - / \u00d7 / \u00f7 after a "paste" appeared to do nothing. Switched both the menu Paste click AND the Cmd/Ctrl+V keyboard shortcut to a single, browser-native window.prompt() dialog. Why this works reliably on every browser and origin: (a) prompt() never touches the clipboard API so it never triggers Safari's clipboard-read permission tooltip on any origin including file://. (b) The prompt's input field is a real focused text input — pressing Cmd/Ctrl+V inside it triggers a NATIVE browser paste into the input, which has zero permission requirements (the user is interacting directly with an editable input). (c) Clicking OK reads input.value synchronously and forwards the string to window.opener.gbCalcSetFromText(text). gbCalcSetFromText (B6.4 #1) strips currency / commas / units and then SIMULATES typing by calling gbCalcPress('ac') followed by a per-digit press loop and an optional press('negate') — so the post-paste calculator state machine is in the exact same mode it would be in if the user had tapped the digits on the calc. Pressing = / + / - / \u00d7 / \u00f7 after a paste therefore behaves identically to typing the same number in. The hidden-textarea / execCommand('paste') / readText paths from B6.3 + B6.4 are bypassed (their helpers stay defined as defensive code but no path calls them on the menu Paste click or the Cmd/Ctrl+V keydown anymore). showPasteHint() likewise stays defined but is no longer called since prompt() never silently fails. End-state behavior on every browser including Safari/file://: \u2022 Cmd/Ctrl+C \u2014 silent copy of the readout. \u2022 Cmd/Ctrl+V \u2014 prompt() opens \u2192 user pastes with native Cmd/Ctrl+V \u2192 OK forwards \u2192 simulate-type \u2192 = / + / - / \u00d7 / \u00f7 work. \u2022 Right-click \u2192 Copy \u2014 silent copy. \u2022 Right-click \u2192 Paste \u2014 prompt() opens \u2192 paste \u2192 OK \u2192 simulate-type \u2192 done. Zero permission tooltips, zero silent failures, zero double-Paste anywhere. (2) VERSION STAMP. Bumped from B6.4 -> B6.5 throughout: title bar (GB-BizCalc - B6.5), version-badge, all PDF headers/footers, every save-as filename (every -B6.4.pdf -> -B6.5.pdf, including cust-info-...-B6.5.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.5" news PDF footer string, internal VERSION constants updated to 'B6.5', AND the JSON export __version__ field now reads 'B6.5'. NO data paths changed: every prior feature from B5.5 through B6.4 (RESET confirm, profile-drag swap confirm, Non-Active Tariffs PDF divider, DIST PDF Eff. Price column move, NO NEW ARTICLE UPDATES popup, calculator copy/paste keyboard path, CUSTOMER INFO panel core, the 12 B5.8 customer-info polish items, the 10 B6.0 customer-info expansion items, the B6.1 PDF column overlap fix + ADDRESSES ONLY contact-location-addresses + right-click menu position, the B6.4 #1 simulate-typing paste state fix, and the B6.4 #3 TARIFF NEWS clickable button + hover state) is preserved byte-for-byte except for the menu Paste click handler, the Cmd/Ctrl+V keydown branch, and the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.4, B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.4May 2026Three changes on top of B6.3 plus the version bump. (1) PASTE-THEN-= NO-OP FIX. gbCalcSetFromText used to set _gbCalc.display + reset accumulator/pendingOp/justDidEq directly and flip _startFresh = true. After a paste the calculator state machine was in an unusual mode — pressing = / + / - / × / ÷ didn't recognize the displayed value as the current operand because no digit-press had touched the state. Now gbCalcSetFromText simulates user typing: gbCalcPress('ac') first to clear the calculator, then a per-digit press loop (gbCalcPress('1'), gbCalcPress('2'), gbCalcPress('.'), …) for each digit and decimal in the cleaned input, then gbCalcPress('negate') if the input was negative. End-state: identical to the user tapping the digits on the calc. Pressing = / + / - / × / ÷ after a paste now works exactly the same as if the user had typed the number — the most-recent operand is in display, the state machine knows about it, and operators read it via _gbCalcToNum(s.display) the way they always do. Currency / commas / whitespace are still stripped before the press loop so a paste like '$1,234.56' correctly enters as 1234.56. (2) ELIMINATE THE SAFARI PASTE TOOLTIP. The B6.3 menu Paste click fell back to navigator.clipboard.readText() when document.execCommand('paste') returned false — and that fallback is what surfaces Safari's OS clipboard-permission tooltip on file:// origins. The tooltip looks like a duplicate Paste menu item to the user. The fallback is removed. Menu Paste click now tries execCommand('paste') only — if it succeeds the hidden textarea's paste event fires and forwards the text silently to gbCalcSetFromText(). If execCommand returns false (Safari, modern Chrome script contexts) we briefly toast a small blue pill at the bottom of the popup reading "Press \u2318V to paste" (or "Ctrl+V" on Windows / Linux). Cmd/Ctrl+V remains the guaranteed no-prompt path: the B6.3 hidden, always-focused textarea + native paste mechanism is unchanged, so the keyboard shortcut still pastes silently with no permission prompt. End-state behavior on Safari/file://: (a) Cmd/Ctrl+C — silent copy. (b) Cmd/Ctrl+V — silent native paste, no prompt. (c) Right-click → Copy — silent copy. (d) Right-click → Paste — silent if execCommand is honored, otherwise a 2.4-second "Press \u2318V to paste" toast and no Safari permission prompt. (3) TARIFFNEWS BUTTON REPLACES SLIDE-DOWN TAB. The yellow .news-peek-tab that used to slide out from under the BREAKING ticker on hover is removed entirely (.news-peek-wrap + .news-peek-tab DOM elements deleted). The existing .ticker-label "Tariff News" pill on the left of the ticker bar — orange-red background, white font — is now itself the clickable entry point: id="tariffNewsLabel", role="button", tabindex="0", onclick=openNewsPopup(), onkeydown handles Enter / Space for keyboard activation. CSS adds cursor:pointer + transition + a :hover rule that flips the background from var(--red) to #fdd835 yellow and the text from white to #1c1c1e black, signalling at a glance that it's a clickable button instead of a static heading. :focus-visible matches the hover styling for keyboard users plus a 2px black outline. The B5.x openNewsPopup() function and the news-popup-backdrop / news-popup-box modal it opens are byte-for-byte unchanged — only the entry point UI is swapped. (4) VERSION STAMP. Bumped from B6.3 -> B6.4 throughout: title bar (GB-BizCalc - B6.4), version-badge, all PDF headers/footers, every save-as filename (every -B6.3.pdf -> -B6.4.pdf, including the cust-info-...-B6.4.pdf family), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.4" news PDF footer string, internal VERSION constants updated to 'B6.4', AND the JSON export __version__ field now reads 'B6.4' per the B5.4 #1 self-advancing literal. NO data paths changed: every prior feature from B5.5 (RESET confirm, profile-drag swap confirm), B5.6 (Non-Active Tariffs PDF divider, DIST PDF Eff. Price column move, NO NEW ARTICLE UPDATES popup), B5.7 (calculator copy/paste keyboard path, CUSTOMER INFO panel core), B5.8 (12 customer-info polish items), B5.9 (Paste menu button), B6.0 (10 customer-info expansion items), B6.1 (PDF column overlap fix, ADDRESSES ONLY contact location addresses, customer-card right-click menu position), B6.2 (Paste menu button restored), and B6.3 (hidden-textarea native-paste mechanism) is preserved byte-for-byte except for the three areas above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.3, B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.3May 2026One change on top of B6.2 plus the version bump. (1) ELIMINATE THE SECOND PASTE TOOLTIP. The B6.2 implementation called navigator.clipboard.readText() on every menu Paste click and every Cmd/Ctrl+V keypress. Safari (and Chrome on file:// URLs that don't persist permission grants) surfaces those calls as a small floating "Paste" permission tooltip near the cursor — the user perceived this as a second Paste menu item that had to be clicked too. Switched to a hidden, always-focused <textarea> placed at fixed:left:-9999px inside the popup body. The textarea has a paste event listener that captures event.clipboardData.getData('text/plain') and forwards the text through window.opener.gbCalcSetFromText() — same parser the prior implementation used (commas / currency / whitespace stripped, finite Number required). Cmd/Ctrl+V no longer calls preventDefault: the keydown handler just makes sure the hidden textarea is focused, then the browser dispatches its NATIVE paste action into the textarea — and native pastes do NOT trigger Safari/Chrome's clipboard-read permission tooltip because it's a real keyboard paste, not a script-initiated clipboard read. Menu Paste click focuses the textarea then tries document.execCommand('paste') which, in browsers that still honor it, fires the textarea's paste event without prompting; if execCommand returns false (modern Chrome blocks it from script in some contexts) we fall back to the prior pasteFromClipboardToCalc() readText path. Refocus logic: mousedown / mouseup / click / window focus / textarea blur all schedule a refocus to the hidden textarea so calculator-button clicks transiently focus the button, then control returns to the textarea so the next Cmd/Ctrl+V hits a focused editable target. pointer-events:none + opacity:0 + tabIndex:-1 + aria-hidden=true keep the textarea invisible and out of tab order. End-state behavior on file:// pages: (a) Cmd/Ctrl+C — silent copy of the readout via writeText(). (b) Cmd/Ctrl+V — silent native paste into the focused hidden textarea, no prompt. (c) Right-click → Copy — silent copy. (d) Right-click → Paste — execCommand path attempted first; silent paste in browsers that honor it. Falls back to readText with the one-time prompt only when execCommand is blocked. Cmd/Ctrl+V remains the guaranteed no-prompt path. (2) VERSION STAMP. Bumped from B6.2 -> B6.3 throughout: title bar (GB-BizCalc - B6.3), version-badge, all PDF headers/footers, every save-as filename (every -B6.2.pdf -> -B6.3.pdf, including the cust-info-...-B6.3.pdf family), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.3" news PDF footer string, internal VERSION constants updated to 'B6.3', AND the JSON export __version__ field now reads 'B6.3'. NO data paths changed — the only diffs from B6.2 are the four blocks of popup-script JS (hidden textarea setup, paste event listener, menu Paste click handler, keydown C/V handler) plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.2, B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.2May 2026One change on top of B6.1 plus the version bump. (1) GB-CALC PASTE MENU ROW RESTORED. The B6.1 #4 removal of the Paste row from the popup right-click menu went too far — the user wanted the duplicate prompt gone, not the menu item itself. The "2nd Paste" the user previously saw was Chrome's clipboard-read permission UI, which appears once per origin / session the FIRST time a script reads the clipboard. After the user clicks Allow / Paste in that prompt, Chrome remembers the grant and subsequent navigator.clipboard.readText() calls run silently. There is no JS API to skip the initial permission grant. The Paste button is back next to Copy in the popup ctx menu (<button id="gbCalcCtxPaste">Paste</button>) and re-wired to call pasteFromClipboardToCalc() on click — the same helper Cmd/Ctrl+V uses. End-state behavior: (a) Right-click → click Copy → display readout writes to system clipboard. (b) Right-click → click Paste → readText() forwards through window.opener.gbCalcSetFromText(text) which strips commas / currency / whitespace and loads a finite Number into the calculator state. First click of a fresh session may surface Chrome's permission tooltip; once granted, subsequent clicks are instant. (c) Cmd/Ctrl+C anywhere in the popup copies the readout — same path as the Copy menu row. (d) Cmd/Ctrl+V anywhere in the popup pastes from the system clipboard — same path as the Paste menu row. The keydown handler special-cases both shortcuts before the generic (metaKey || ctrlKey || altKey) early-return so the rest of the keyboard input map (digits, +, -, *, /, =, Enter, %, Backspace, Escape) is unaffected. The contextmenu handler still preventDefaults the OS native menu so only our custom Copy / Paste rows show. (2) VERSION STAMP. Bumped from B6.1 -> B6.2 throughout: title bar (GB-BizCalc - B6.2), version-badge, all PDF headers/footers, every save-as filename (every -B6.1.pdf -> -B6.2.pdf, including the cust-info-...-B6.2.pdf family), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.2" news PDF footer string, internal VERSION constants updated to 'B6.2', AND the JSON export __version__ field now reads 'B6.2' per the B5.4 #1 self-advancing literal. NO data paths changed: every prior feature from B5.5 (RESET confirm, profile-drag swap confirm), B5.6 (Non-Active Tariffs PDF divider, DIST PDF Eff. Price column move, NO NEW ARTICLE UPDATES popup), B5.7 (calculator copy/paste keyboard path, CUSTOMER INFO panel core), B5.8 (12 customer-info polish items), B5.9 (Paste menu button — now restored as B6.2 #1), B6.0 (10 customer-info expansion items incl. Company URL, contact-X delete confirm, US/CA progressive phone formatting, country picker + Linked-In, green-dot-inside checkbox, sticky HQ suffix, brighter orange PDF, Rep Co. URL, per-contact PDF), and B6.1 (PDF column overlap fix, ADDRESSES ONLY contact location addresses, customer-card right-click menu position) is preserved byte-for-byte except for the two lines of ctx-menu DOM and four lines of click wiring above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.1, B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.1May 2026Four fixes on top of B6.0 plus the version bump. (1) CUSTOMER-INFO PDF COLUMN OVERLAP. The lab() helper inside _roiCInfoPdfBuild placed the value text at a fixed M+92 X coordinate which wasn't enough room for long labels like "Phone 2 (Biz - Direct, US):" — the value overlapped the label. lab() now measures the rendered label width with doc.getTextWidth() and places the value at M + max(150, labelWidth + 12) so there's always a clear gap. Long values that would otherwise run past the right margin are wrapped onto subsequent lines via doc.splitTextToSize using the remaining (W - M - xVal) width, with each wrapped line indented to the same value column. ensure(13) is called per wrapped line so wrap-overflow never crosses a page boundary mid-line. Affects every PDF emitted by the customer info panel: the five top-row modes (ALL CUST. INFO, CUST. INFO - NO NOTES, ALL CONTACTS, ALL CONTACTS - NO NOTES, ADDRESSES ONLY) and the per-contact PDF export added in B6.0 #10. (2) ADDRESSES ONLY MODE NOW INCLUDES CONTACT LOCATION ADDRESSES. The B5.8 #12 ADDRESSES ONLY download mode used to emit only the Company Address + Shipping Address sections. It now also iterates each contact and, when contact.locAddress has any non-empty field, emits a "CONTACT LOCATION ADDRESSES" section header (once, only if at least one contact has a location address) followed by per-contact sub-sections each titled with the Location text (or "Contact N Location" fallback if the location field is blank but the address is filled in) and the address lines below it via _roiCInfoFmtAddrLines. Per spec: ADDRESSES ONLY now matches "a PDF of all addresses with the company name at the top and the Location name above each sub-address from each address CONTACT ADDRESS". The other four PDF modes already emitted full per-contact data (location addresses included) and are unchanged. (3) CUSTOMER CARD RIGHT-CLICK MENU POSITION. The 4-row menu (DELETE CUST. CARD / CUSTOMER INFO / DUPLICATE CARD / PDF - ALL COST BASIS) was positioned with the cursor at the menu's vertical center, which put the cursor between the CUSTOMER INFO and DUPLICATE CARD rows. The user expected CUSTOMER INFO (row 2) to sit directly under the cursor since that's the common path. Position math now computes the CUSTOMER INFO row's offsetTop + offsetHeight/2 (located via menu.querySelector('button[onclick*="customerInfo"]')) and shifts the menu down so that point sits at evt.clientY. Falls back to the prior centered behavior if the CUSTOMER INFO button isn't found (defensive). The viewport-clamping logic on nx/ny still runs after, so a right-click near the bottom edge still snaps the menu inside the window without breaking the row alignment. (4) CALCULATOR PASTE MENU ROW REMOVED. The B5.9 #1 Paste row in the GB-Calc popup ctx menu is removed. Reason: clicking it called navigator.clipboard.readText() which Chrome surfaces as a small floating "Paste" permission tooltip near the cursor — the user perceived this as a duplicate Paste option requiring a second click to actually paste. The right-click menu is now Copy-only. Cmd/Ctrl+V keyboard shortcut from B5.7 #1 stays wired for the paste path — the paste handler (pasteFromClipboardToCalc) and main-side gbCalcSetFromText are unchanged. Rationale matches the user's explicit request: "remove that 2nd paste option — it only needs 1 paste click to paste". (5) VERSION STAMP. Bumped from B6.0 -> B6.1 throughout: title bar (GB-BizCalc - B6.1), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B6.0.pdf -> -B6.1.pdf, including the cust-info-...-B6.1.pdf family from B5.8 #12 / B6.0 #10), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.1" news PDF footer string, internal VERSION constants updated to 'B6.1', AND the JSON export __version__ field now reads 'B6.1' per the B5.4 #1 self-advancing literal. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, news fetcher (with the B5.6 #3 prior-first dedupe), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card (with the B5.5 #2 confirm), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, the B5.3 #3 modal-head star, the B5.4 #1 JSON __version__ self-advancing literal, the B5.5 #1 RESET confirm, the B5.5 #2 profile drag-drop swap confirm, the B5.6 #1 Non-Active Tariffs PDF divider, the B5.6 #2 DIST PDF Eff. Price column move, the B5.6 #3 NO NEW ARTICLE UPDATES popup, the B5.7 #1 calculator copy/paste keyboard path, the B5.7 #2 CUSTOMER INFO panel core, the 12 B5.8 customer-info polish items, and the 10 B6.0 customer-info expansion items (Company URL, contact-X delete confirm, US/CA progressive phone format, country picker + Linked-In, green-dot-inside checkbox, sticky HQ suffix, brighter orange PDF, Rep Co. URL, per-contact PDF) are all byte-for-byte identical to B6.0 except for the four areas above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B6.0, B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B6.0May 2026Ten CUSTOMER INFO panel enhancements on top of B5.9 plus the version bump. (1) COMPANY URL FIELD. New "Company URL" row directly under Company Name, stored as data.companyUrl, autosaved on every keystroke just like the rest of the panel. (2) PER-CONTACT X DELETE BUTTON. Each contact past Contact 1 now renders an X button right of the Notes / PDF buttons in the contact heading. Clicking it opens a center-screen native window.confirm("CONFIRM CONTACT INFO DELETION"). On OK the contact is spliced out; on Cancel nothing changes. Contact 1 has no X (required slot). The B5.8 #1 right-click "CONFIRM CONTACT DELETION" path remains in place unchanged — user has both routes. (3) USA PHONE AUTO-FORMAT. Typing into any US/CA phone input now formats live as the user types: 1-3 digits stay raw, 4-7 digits become NNN-NNNN (123-4356), 8-10 digits become (NNN) NNN-NNNN ((661) 234-4356), 11+ digits become +CC (NNN) NNN-NNNN with the leading country-code digits prefixed and any extras appended. The cursor is repositioned to the same digit index after each reformat so backspacing / mid-string edits don’t feel jumpy. The format runs through _roiCInfoApplyPhoneFmt() which mutates input.value, persists the formatted string into data.contacts[i].phones[j].num, and writes through to localStorage. (4) COUNTRY PICKER + LINKED-IN ROW. The Phone label is now a clickable button (.cinfo-phone-lbl) that opens a small sub-modal "SELECT COUNTRY" (z-index 2000, over the Customer Info modal) with a search input at the top and a scrollable list of 38 major countries (US, CA, MX, GB, DE, FR, IT, ES, PT, NL, BE, IE, CH, AT, SE, NO, DK, FI, IS, GR, PL, CZ, HU, JP, KR, CN, TW, HK, SG, AU, NZ, IL, AE, IN, BR, AR, CL, TR, RU). Each row shows the flag emoji + country name + ISO code + dial prefix; current selection is highlighted in green. The search filters by name, code, or dial prefix. Picking a country sets data.contacts[i].phones[j].country, reformats the existing number with the new country’s template (US/CA use the progressive formatter from #3; every other country uses a per-country digit template like "(##) #####-####" for BR or "### #### ####" for CN), and re-renders so the Phone button label shows the new flag. The label is per-phone so a contact can have a US cell + a UK biz line each with its own format. A new "Linked-In" row sits directly under the phone rows in every contact, autosaved into contact.linkedIn. (5) GREEN-DOT-INSIDE CHECKBOX FOR HQ / SHIPPING / REP. The three .cinfo-cb checkboxes (HQ, Shipping Address, Rep) are re-styled with appearance:none plus a 9px filled green dot centered inside the box when the checkbox is :checked. The B5.8 #2 outside-left orange has-data dot is removed (the has-data state still applies the green-dot-inside indicator so the user sees the dot when there’s saved data behind an unchecked Shipping or Rep box). The HQ checkbox has no has-data state since the suffix is the indicator. (7) HQ STICKY SUFFIX. When isHQ is on for a card, the " - HQ" suffix can no longer be removed by typing. The companyName oninput handler intercepts each keystroke: if isHQ is true and the new value doesn’t end with " - HQ", the handler re-applies the suffix and resets the input.value (preserving cursor position before the suffix). Same enforcement runs in roiOnCustomerCardNameInput so the customer card slot input is also locked. The only way to remove the suffix is to uncheck the HQ box. The slot input’s maxlength was bumped 17 → 22 to fit a 17-char base name + the 5-char suffix. Auto-detection (typing " - HQ" flips isHQ on) is preserved when isHQ is currently off. (8) PDF BUTTON COLOR. The top-row .cinfo-pdf-btn background changed from #ff9f0a (the SF amber that read as yellow next to other orange buttons in the app) to #ff7a00 with a #cc5800 border, matching the orange family used by .ctx-pdf-allbasis (the per-card PDF button) and the per-basis PDF buttons throughout the ROI-Calc tab. White font preserved. (9) REP CO. URL ROW. When the Rep checkbox is on, a new "Rep Co. URL" input row appears directly under the "Rep Company / Name for Company Name" row, autosaved into contact.repUrl. Disappears with the rest of the rep block when Rep is unchecked, but the value is preserved in storage so re-checking Rep restores everything as it was. (10) PER-CONTACT PDF EXPORT. The contact heading now also shows a small orange "PDF" button (.cinfo-contact-pdf, same #ff7a00 family as the top-row PDF button) right of the Notes button. Clicking it calls roiCInfoExportContactPDF(idx) which uses the existing jsPDF pipeline (factored into _roiCInfoPdfBuild) to emit a single-contact PDF: title bar with company name, the contact’s title / name / location / location address / emails / phones (with country code + type) / Linked-In / rep info / notes. Filename: cust-info-<company>-contact-N-<first>_<last>-B6.0.pdf. The five-mode top-row PDF picker from B5.8 #12 stays intact and now also includes the new fields. (11) VERSION STAMP. Bumped from B5.9 -> B6.0 throughout: title bar (GB-BizCalc - B6.0), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B5.9.pdf -> -B6.0.pdf, including the cust-info-...-B6.0.pdf family), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B6.0" news PDF footer string, internal VERSION constants updated to 'B6.0', AND the JSON export __version__ field now reads 'B6.0' per the B5.4 #1 self-advancing literal. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, news fetcher (with the B5.6 #3 prior-first dedupe), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card (with the B5.5 #2 confirm), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, the B5.3 #3 modal-head star, the B5.4 #1 JSON __version__ self-advancing literal, the B5.5 #1 RESET confirm, the B5.5 #2 profile drag-drop swap confirm, the B5.6 #1 Non-Active Tariffs PDF divider, the B5.6 #2 DIST PDF Eff. Price column move, the B5.6 #3 NO NEW ARTICLE UPDATES popup, the B5.7 #1 calculator copy/paste keyboard path, the B5.7 #2 CUSTOMER INFO panel core, the 12 B5.8 customer-info polish items, and the B5.9 calculator Paste menu button are all byte-for-byte identical to B5.9 except for the customer-info module + its CSS + the slot-input maxlength + the slot-input sync hook plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.9, B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.9May 2026One small change on top of B5.8 plus the version bump. (1) GB-CALC RIGHT-CLICK PASTE BUTTON. The Cmd/Ctrl+V keyboard paste path was already fully wired in B5.7 (popup keydown listener calls navigator.clipboard.readText() and forwards the string to window.opener.gbCalcSetFromText() which strips commas / currency symbols / whitespace, parses to a finite Number, and loads it into the calculator state via _gbCalcFmt() + _gbCalcSave() + _gbCalcRender()), but the right-click context menu only had a single "Copy" row, so the paste path wasn't discoverable. The popup ctx menu DOM now has a "Paste" button below "Copy", wired to the existing pasteFromClipboardToCalc() helper (reads navigator.clipboard.readText(), forwards to opener). No behavior change to the copy path or the keyboard shortcuts; Cmd/Ctrl+C still copies, Cmd/Ctrl+V still pastes, and the new menu row is just a discoverable mouse-driven equivalent. The existing menu sizing (min-width 90px) accommodates the second row without re-tuning. (2) VERSION STAMP. Bumped from B5.8 -> B5.9 throughout: title bar (GB-BizCalc - B5.9), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B5.8.pdf -> -B5.9.pdf, including the cust-info-...-B5.9.pdf family from B5.8 #12), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.9" news PDF footer string, internal VERSION constants updated to 'B5.9', AND the JSON export __version__ field now reads 'B5.9' per the B5.4 #1 self-advancing literal. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, news fetcher (with the B5.6 #3 prior-first dedupe), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card (with the B5.5 #2 confirm), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, the B5.3 #3 modal-head star, the B5.4 #1 JSON __version__ self-advancing literal, the B5.5 #1 RESET confirm, the B5.5 #2 profile drag-drop swap confirm, the B5.6 #1 Non-Active Tariffs PDF divider, the B5.6 #2 DIST PDF Eff. Price column move, the B5.6 #3 NO NEW ARTICLE UPDATES popup, the B5.7 #1 calculator copy/paste keyboard path, the B5.7 #2 CUSTOMER INFO panel core, and all 12 B5.8 customer-info polish items (right-click contact delete confirm, has-data dot on Shipping/Rep checkboxes, card-name <-> company-name bidirectional sync, HQ suffix, Location field, email/phone delete confirms, unbounded Title input, renamed phone types Biz - Main / Biz - Direct, HQ + Shipping moved into Company Address header, Notes + PDF buttons, Address dropdown next to Location with light-green has-data state and duplicated GPS button, 5-mode PDF export) are all byte-for-byte identical to B5.8 except for the two-line ctx-menu DOM + two-line script wiring above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.8, B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.8May 2026Twelve polish items on top of the B5.7 CUSTOMER INFO panel plus the version bump. (1) RIGHT-CLICK CONTACT DELETE WITH CONFIRM. The standalone X button on each contact frame is removed. Right-clicking anywhere on a contact (excluding inputs/buttons) now opens a center-screen native window.confirm("CONFIRM CONTACT DELETION"). On OK the contact is spliced out of data.contacts; on Cancel nothing changes. Contact 1 is required and silently no-ops on right-click (matching the pre-B5.8 lock on removing the first contact). Same iOS-style alert pattern as the other B5.x deletion confirms. (2) LARGE DOT INDICATOR ON SHIPPING + REP CHECKBOXES WITH DATA. .cinfo-cb gets a new .has-data state that renders an absolutely-positioned 9px filled orange dot inside the label, left of the checkbox text. The dot appears whenever the underlying data tree has content even if the checkbox itself is unchecked: the Shipping Address checkbox lights when any shippingAddress field is non-empty (regardless of shippingEnabled), and the Rep checkbox lights when isRep is on AND repCompany has a non-empty trimmed value. Lets the user see at a glance that there's saved data behind a collapsed control. (3) CARD NAME <-> COMPANY NAME BIDIRECTIONAL SYNC. On panel open the customer card's .name is mirrored into data.companyName (overwriting if the card name is non-empty so rename-then-reopen always reflects the latest typed value). Edits to companyName inside the panel push back through roiCardsSave + roiRenderCustomerCards (guarded by an internal _roiCInfoSyncing flag so the re-render doesn't recurse). Edits typed into the customer card name input call a new _roiCInfoSyncFromCardName(cardId, name) hook that re-saves the cinfo record and re-renders the open panel if it's showing this card. (4) HQ CHECKBOX APPENDS / STRIPS " - HQ" SUFFIX. Toggling HQ ON applies _roiCInfoApplyHq() to companyName (no-op if already ends with " - HQ" so we don't double-append). Toggling HQ OFF strips the suffix via _roiCInfoStripHq(). The customer card .name is updated through the same sync path so the slot input shows the suffixed name too. The maxlength="17" on the slot input only restricts user typing; programmatic value can exceed it so the suffix renders even on long names. Auto-detection: typing a name ending in " - HQ" into either input also flips isHQ on; removing the suffix flips it back off. (5) NEW "LOCATION" FIELD. Each contact gets a new location text input rendered between Title and Name. Stores in contact.location. Used as the heading text for that contact's optional Location Address dropdown. (6) DELETE-EMAIL / DELETE-PHONE CONFIRMS. Clicking the X next to an additional email row opens window.confirm("CONFIRM TO DELETE EMAIL ADDRESS") and only splices the email out on OK. Phone X opens window.confirm("CONFIRM TO DELETE PHONE NUMBER"). Same iOS-style alert pattern. (8) UNBOUNDED TITLE INPUT. The Title input's maxlength=40 cap is removed; it now accepts unlimited text. The .cinfo-title-input rule sets max-width:none and overflow-x:auto so long titles overflow horizontally inside the input itself instead of breaking the row layout. (9) PHONE TYPE LIST RENAMED. ROI_CINFO_PHONE_TYPES is now ['Biz - Main', 'Biz - Direct', 'Cell', 'Fax']. ROI_CINFO_PHONE_DEFAULT is 'Biz - Main'. _roiCInfoNormalize() migrates legacy records: any phone with type 'Main' becomes 'Biz - Main', 'Biz' becomes 'Biz - Direct', and any unrecognized type falls back to the default. The select column width is bumped to 110px to fit the longer labels. (10) HQ CHECKBOX MOVED TO COMPANY ADDRESS HEADER ROW. The HQ label is removed from the top Company Name row and now lives inside .cinfo-section-head for the Company Address section, right of the GPS button, so the HQ flag sits with the address data it modifies. Section heads were updated with flex-wrap and gap so the heading + GPS + checkboxes lay out cleanly even on narrow widths. (11) SHIPPING-ADDRESS CHECKBOX MOVED TO COMPANY ADDRESS HEADER ROW. Sits to the right of HQ in the same .cinfo-section-head flex strip. Toggling on still expands the Shipping Address section in its own .cinfo-section block UNDER the Company Address section, exactly like B5.7 — only the toggle's location moved. The Shipping label gets the new B5.8 #2 has-data dot when shippingAddress has any non-empty field. (12) NOTES + PDF + ADDRESS-DROPDOWN BUTTONS. Three new things: (a) A "Notes" button next to the Company Name input and another next to each Contact heading. Each opens a small sub-modal (z-index 2000, over the Customer Info modal) with a large textarea bound to data.companyNotes or data.contacts[i].notes; every keystroke autosaves and the button picks up the .has-data orange tint when the linked notes field has any non-whitespace content. Sub-modal dismisses on X, backdrop click, or Esc and remembers what was typed. (b) A small orange "PDF" button next to the top-row Notes button opens a "SELECT PDF DOWNLOAD OPTIONS" sub-modal with five choices wired to roiCInfoExportPDF(mode): ALL CUST. INFO (everything including notes), CUST. INFO - NO NOTES, ALL CONTACTS, ALL CONTACTS - NO NOTES, ADDRESSES ONLY. The exporter uses jsPDF (already loaded for tariff / ROI PDFs) and emits a single document with the company name as the title, 0.6pt rule, then the requested sections; per-mode flags toggle whether company info, contacts, or notes are emitted; ADDRESSES ONLY emits company name + Company Address + Shipping Address only. Output filename: cust-info-<company>-<mode>-B5.8.pdf. (c) Each contact's Location row gets an Address dropdown toggle button. Clicking it expands a per-contact address sub-section (street / street2 / city / state / zip / country) below the Location row, autosaved into contact.locAddress. The toggle button turns light green (.has-data) when any locAddress field has content, and a duplicate GPS button appears next to the toggle when the dropdown is COLLAPSED + has data so the user can jump to Maps without expanding. The dropdown also has its own GPS button inside the section header for symmetry with Company / Shipping Address. State is fully persisted (locAddrOpen flag remembers whether the user had it expanded across re-renders). (13) VERSION STAMP. Bumped from B5.7 -> B5.8 throughout: title bar (GB-BizCalc - B5.8), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B5.7.pdf -> -B5.8.pdf, plus the new cust-info-...-B5.8.pdf family from #12), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.8" news PDF footer string, internal VERSION constants updated to 'B5.8', AND the JSON export __version__ field now reads 'B5.8' per the B5.4 #1 self-advancing literal. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, news fetcher (with the B5.6 #3 prior-first dedupe), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card (with the B5.5 #2 confirm), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, the B5.3 #3 modal-head star, the B5.4 #1 JSON __version__ self-advancing literal, the B5.5 #1 RESET confirm, the B5.5 #2 profile drag-drop swap confirm, the B5.6 #1 Non-Active Tariffs PDF divider, the B5.6 #2 DIST PDF Eff. Price column move, the B5.6 #3 NO NEW ARTICLE UPDATES popup, the B5.7 #1 calculator copy/paste, and the B5.7 #2 CUSTOMER INFO panel core (the 12 items above are additive on top of that core) are all byte-for-byte identical to B5.7 except for the customer-info module + its CSS + the roiOnCustomerCardNameInput sync hook plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.7, B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.7May 2026Two new features on top of B5.6 plus the version bump. (1) GB-CALC CALCULATOR COPY/PASTE. The floating GB-Calc popup window now supports both right-click copy and the standard OS-level keyboard shortcuts on the readout. Inside the popup document: a new oncontextmenu handler on the body opens a small dark Apple-style popover at the cursor with a single "Copy" row; clicking it writes the current readout (the contents of #gbCalcDisplay, trimmed) to the system clipboard via navigator.clipboard.writeText() with a hidden-textarea + document.execCommand('copy') fallback for older engines. The popover anchors to the click point and clamps to the popup viewport; clicking elsewhere or pressing any other key dismisses it. The keydown listener now special-cases Cmd/Ctrl+C (any time the popup has focus) to call the same copy helper, and Cmd/Ctrl+V to call navigator.clipboard.readText() and forward the resulting string to a new main-side helper window.opener.gbCalcSetFromText(text). gbCalcSetFromText() strips commas / currency symbols / whitespace / units and parses the leftover digits + decimal + leading minus into a finite Number, then loads it into the calculator state (_gbCalc.display via _gbCalcFmt(), tokens reset, accumulator + pendingOp + justDidEq cleared, _startFresh true so the next digit press starts a new operand) and re-renders. Pasting outside the calculator (other apps / cells / inputs) just uses whatever the system clipboard has — because we wrote there on copy, native Cmd/Ctrl+V works in any external target with no special path. Both the right-click and the keyboard handlers work at the document level so they fire regardless of which popup element has focus. The previous early-return on (metaKey || ctrlKey || altKey) inside the keydown listener now runs only AFTER the new C/V cases so the rest of the keyboard shortcuts (digits, +, -, *, /, =, Enter, %, Backspace, Escape) are unchanged. (2) CUSTOMER INFO PANEL. New entry "CUSTOMER INFO" in the Customer Card right-click menu, vertically between DELETE CUST. CARD (top) and DUPLICATE CARD (below it). Either clicking the menu row OR double-clicking anywhere on a Customer Card frame opens the new panel directly. Double-click intentionally short-circuits if the click target is the name input or any inner button so existing text-edit / lock-toggle behaviors still work normally. Panel UI is a center-screen modal (580px wide, max 88vh tall, scrollable body) with a backdrop overlay (z-index 1700) that darkens everything beneath; the modal itself sits at z-index 1800 so it overlays every other menu and popover in the app. Header has a CUSTOMER INFO title with the matched card name appended after " — " (updates as the user types in the name input on the card), and an X close button on the right; clicking the X or the backdrop closes the panel; pressing Esc also closes. Form layout (top to bottom): Company Name (80-char) with an HQ checkbox to its right (per spec, for chain-of-stores HQ identification); Company Address with Street / Street 2 / City / State-Province / ZIP-Postal / Country fields and a blue GPS button at the section header that opens https://www.google.com/maps/search/?api=1&query=encoded(addr) in a new tab when any field is filled (alert if all fields are blank); a "Shipping Address (If different)" checkbox that, when ticked, expands the same address block + GPS button below it (collapsed by default so it doesn’t waste space when not needed); a Contacts list starting with Contact 1 (required — cannot be removed) followed by an + Add Contact button at the bottom that appends Contact 2, Contact 3, etc. Each contact has its own framed sub-section with: Title input (maxlength 40 per spec, rendered ~340px wide so 40 chars comfortably fit) before the name; Rep checkbox to the right of Title; First Name + Last Name on the next row; up to 2 emails on consecutive rows with a + button on the last row to add the 2nd email and an × to remove anything past Email 1; up to 4 phones on consecutive rows each with its own type selector (Main / Biz / Cell / Fax) so the saved number is unambiguous, with the same + and × pattern. When Rep is ticked an extra row appears immediately under Title reading "Rep Company / Name" (editable) followed by an italic "for" and the Company Name from the top of the panel (read-only — so unticking Rep restores the standard Company-name view without losing the typed Rep value). Every input autosaves on each keystroke / change to localStorage under gbBizCalcCustomerInfo_v1 keyed by the Customer Card id (no save button anywhere, matching the rest of the app’s autosave pattern). Removing a contact, email, or phone splices it out of the array; removing the only entry restores a blank one. The panel persists data even on accidental backdrop click — close just hides the modal, the underlying record stays intact for the next open. (3) VERSION STAMP. Bumped from B5.6 -> B5.7 throughout: title bar (GB-BizCalc - B5.7), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B5.6.pdf -> -B5.7.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.7" news PDF footer string, internal VERSION constants updated to 'B5.7', AND the JSON export __version__ field now reads 'B5.7' in every new gb-bcalc-MM-DD-YY[a-z].json. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, news fetcher (with the B5.6 #3 prior-first dedupe), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card (with the B5.5 #2 confirm), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, the B5.3 #3 modal-head star, the B5.4 #1 JSON __version__ self-advancing literal, the B5.5 #1 RESET confirm, the B5.5 #2 profile drag-drop swap confirm, the B5.6 #1 Non-Active Tariffs PDF divider, the B5.6 #2 DIST PDF Eff. Price column move, and the B5.6 #3 NO NEW ARTICLE UPDATES popup are all byte-for-byte identical to B5.6 except for the two areas above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.6, B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.6Apr 2026Three changes on top of B5.5 plus the version bump. (1) EACH-TARIFF-EXPLAINED PDF: NON-ACTIVE TARIFFS SECTION DIVIDER. In the User Guide §9 “Each Tariff Explained” PDFs (per-route PDF, ALL TARIFFs PDF, and the Full Guide PDF that walks the same route panels), the H3 that introduces the "Non-Active Tariffs" subsection now gets two visual differentiators so the divide between active and non-active tariffs reads at a glance instead of looking like just another tariff entry. First, _ugPdfHeading() now adds an extra ~14pt of pre-spacer above the heading (only when the heading text matches case-insensitive "Non-Active Tariffs") so there’s clear breathing room between the last active-tariff entry and the section break. Second, immediately under the heading text the function draws a thin (0.6pt) light-gray (RGB 200/200/200) horizontal rule that spans the full printable width of the page from the left margin (M) to the right margin (W-M), then advances ctx.y by 8pt so the first "Non-Active Tariffs" paragraph sits below the rule with a normal gap. _ugPdfEnsure() includes the extra room in its page-break check so the heading + rule never split across a page boundary. The rule color and width are scoped via doc.setDrawColor / setLineWidth right before doc.line and the function does not reset them on exit — follow-on draws don’t rely on those defaults so this is safe. Every other H2/H3 in the User Guide PDFs is unchanged — only the literal text "Non-Active Tariffs" (lvl 2) triggers the new path. (2) DISTRIBUTOR EDITION ROI PDF: EFF. PRICE COLUMN MOVED LEFT. The DIST edition (gripblades-roi-...-dist-Bx.y.pdf, generated by the per-card / ALL COST BASIS ROI PDF exporters with isDist=true) rendered the Eff. Price header and per-row value at align=right, pinned to the right margin (W-M). Because the DIST edition only shows Tier / Price / Promo % / Eff. Price (no GripBlades $ Profit + ROI % columns), this left a wide empty void between the Promo % column at M+155 and the Eff. Price column hanging off the far right edge of the page. Both the column header and each row value now print left-aligned at M+220 — the same 65pt gap from Promo % (M+155) that Price (M+90) has from Promo %, mirrored on the right side. So the columns now read Tier (M) | Price (M+90) | Promo % (M+155) | Eff. Price (M+220), evenly spaced left-to-right with no awkward right-side gap. The full edition (non-DIST) is unchanged — it keeps Eff. Price at M+215, GripBlades $ Profit at M+290, and ROI % right-aligned at W-M. The data computation paths (raw × (1-promo/100), the cardLanded-null branch, the fmt2 / sfmt2 / pct2 helpers, A8.8 promo-yellow text color, the B1.1 #2 dist-section header row) are all byte-for-byte identical to B5.5 — only the X coordinate changed. (3) UPDATE NEWS DEDUPE + "NO NEW ARTICLE UPDATES" POPUP. mergeNewsItems() used to process newItems FIRST then priorItems SECOND, which meant any AI response that returned the same article on a subsequent UPDATE NEWS click silently re-stamped the prior copy with a fresh __fetchedAt = ts — making old articles light up the green-dot "new" indicator and add a fresh tick to the horizontal timeline slider on every refresh, even though no genuinely new article had appeared. Two fixes: (a) the merge order is inverted — priorItems FIRST (preserving each article’s existing __fetchedAt, or 0 if it never had one), then newItems SECOND (skipping any that match a prior via the existing _newsDedupeKeys multi-key dedupe — headline-trim + summary-trim + source_url-trim + the per-item fingerprint hash). The merged array is then sorted by __fetchedAt descending so the visual order in the grid stays newest-first as before. (b) The function stashes the count of truly-fresh adds on mergeNewsItems._lastFreshCount so loadNews() can read the exact count of unique new articles (replacing the pre-B5.6 priorHeadlines.has(headline) diff which only checked one of the four dedupe keys and missed dupes that changed wording but kept the same source_url). When that count comes back as 0 on a userInitiated refresh, loadNews() now opens a center-screen native window.alert("NO NEW ARTICLE UPDATES") in addition to the existing "✓ Up to date — no new articles" pill next to the UPDATE NEWS button. Same iOS-style alert pattern as B5.1 #1 / B5.2 #2 / B5.3 #2 / B5.5 #1 / B5.5 #2. NOTE: the cache write (setNewsCache(merged)) still happens on every refresh because the priorItems’ own __fetchedAt fields are the source of truth for the timeline — we just don’t bump them anymore on a no-op refresh. The NEWS_CACHE_TTL = 90-day freshness gate, the NEWS_MAX_ITEMS = 25 cap, and the FIFO drop-oldest behaviour from B3.8 #2 are all unchanged. (4) VERSION STAMP. Bumped from B5.5 -> B5.6 throughout: title bar (GB-BizCalc - B5.6), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B5.5.pdf -> -B5.6.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.6" news PDF footer string, internal VERSION constants updated to 'B5.6', AND the JSON export __version__ field now reads 'B5.6' in every new gb-bcalc-MM-DD-YY[a-z].json the app downloads (per the B5.4 #1 self-advancing literal). The JSON daily-letter naming convention from B4.9 #2 is unchanged — still emits gb-bcalc-MM-DD-YY[a-z].json with the B5.3 #1 post-load advance applied. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (now with the B5.6 #3 prior-first dedupe), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card (with the B5.5 #2 confirm), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export (now with the B5.6 #2 DIST Eff. Price column position), the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, the B5.3 #3 modal-head star, the B5.4 #1 JSON __version__ self-advancing literal, the B5.5 #1 RESET confirm, and the B5.5 #2 profile drag-drop swap confirm are all byte-for-byte identical to B5.5 except for the three areas above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.5, B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.5Apr 2026Two changes on top of B5.4 plus the version bump. (1) RESET BUTTON CONFIRM ON 10/40 PACK PANELS. The Reset button on each SKU card inside the 10 Pack, 10 Pack MC, 40 Pack and 40 Pack MC panels (the small button at the top-right of .roi-card-body wired to onclick="roiResetSku(${i})") used to clear all data for the currently-selected Customer Card × Profile × Macro Cost Basis instantly the moment it was clicked. roiResetSku() now opens a center-screen native window.confirm("CONFIRM YOU WANT TO RESET & CLEAR THE DATA") at the very top of the function and bails out immediately on Cancel. Only on OK does the function proceed to wipe the SKU’s entries from roiState[roiBasis][mpn], roiPromos[roiBasis][mpn], the active sub-profile’s snapshot.state / .promos for that basis, this card’s notepad via roiClearNotesForCard(), every #roiGrid input data-sku="${i}", the four A/B/C/D promo badges (.has-promo + active readout), then re-run roiRecalc() and roiRenderCustomerCards() to refresh the Sub Profile has-data dot. Same iOS-style alert pattern as the B5.1 #1 tariff-history confirm, the B5.2 #2 timeline-node confirm, and the B5.3 #2 article-delete confirm. A single one-line gate at the top of roiResetSku() covers all four basis panels because all four render the same SKU card with the same onclick handler — only the roiBasis bucket differs. The basis-level LANDED-$/pc input on the Macro Cost Basis option (which the function intentionally does NOT clear, per its own comment) remains untouched on either Cancel or OK. (2) PROFILE DRAG-DROP DATA-SWAP CONFIRM. Inside any Customer Card, dragging a Profile (sub-profile button) onto another Profile used to call roiApplySubProfileSnapshot(tgtCardId, tgtSubId, snap) the instant the drop fired — silently overwriting the target Profile’s data tree (state, promos, notes, lock state when applicable) with a deep copy of the source Profile’s snapshot. roiSubDrop() now wraps that apply-call in a center-screen native window.confirm("CONFIRM DATA SWAP / DROP") so the swap only happens after explicit OK; on Cancel the target Profile’s data is left untouched. Drag visuals (the .roi-sub-dragging / .roi-sub-drop-hover classes plus the four clipboard slot classes) are cleared via roiSubDragEnd(evt) AFTER the confirm either way so the UI doesn’t leave a stuck drop-hover glow on Cancel. The new gate also covers the Clipboard → Profile path through the same roiSubDrop() function (when _roiDragClipboardFrom >= 0 the snap is JSON-cloned out of the indexed clipboard slot before the same roiApplySubProfileSnapshot() call) — dropping a clipboarded snapshot onto a Profile now also requires confirm. Same-source (srcCardId === tgtCardId && srcSubId === tgtSubId) and the B5.4 #3 locked-target Profile bail still short-circuit BEFORE the confirm so identical drops and locked targets silently no-op with no popup. The Macro Cost Basis drag-drop swap (roiBasisDrop → _roiShowBasisSwapConfirm) and the Customer Card drag-drop reorder + swap (roiCardDrop / _roiShowCardSwapConfirm) flows already had their own confirm UIs and are unchanged. The Customer Card drag-onto-Profile reorder path is also unchanged — reorder is not a data swap. (3) VERSION STAMP. Bumped from B5.4 -> B5.5 throughout: title bar (GB-BizCalc - B5.5), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B5.4.pdf -> -B5.5.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.5" news PDF footer string, internal VERSION constants updated to 'B5.5', AND the JSON export __version__ field now reads 'B5.5' in every new gb-bcalc-MM-DD-YY[a-z].json the app downloads (per the B5.4 #1 fix the literal now lives in active code and advances on every bump). The JSON daily-letter naming convention from B4.9 #2 is unchanged — still emits gb-bcalc-MM-DD-YY[a-z].json with the B5.3 #1 post-load advance applied. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card (now with the B5.5 #2 confirm), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, the B5.3 #3 modal-head star, and the B5.4 #1 JSON __version__ self-advancing literal are all byte-for-byte identical to B5.4 except for the two areas above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.4, B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.4Apr 2026One bug fix on top of B5.3 plus the version bump. (1) JSON EXPORT __version__ FIELD NOW UPDATES ON EVERY VERSION BUMP. The exportAllAppData() function builds a JSON payload with a small header at the top: { __format__: 'gb-bizcalc-export-v1', __version__: 'Bx.y', __exportedAt__: <ISO timestamp>, keys: { ... } }. The __version__ field had been riding along as a hardcoded literal 'B0.8' ever since that release — the standard bumper does a literal BX.Y -> BA.B replace across active code, so a frozen-at-'B0.8' string was invisible to it and never advanced. Result: every JSON download from B0.9 through B5.3 inclusive showed "__version__": "B0.8" at the top, even though the rest of the app (title bar, all PDF headers/footers, every save-as filename, the User Guide changelog, the version-badge, every internal const VERSION) was correctly stamped with the current release. Fix in two parts: _b54_fixes.py rewrote the literal from 'B0.8' to the current 'B5.3', then this bumper advanced it to 'B5.4' as part of the standard B5.3 -> B5.4 sweep. Going forward the string lives in active code as a normal current-version literal so every future bumper run catches it automatically alongside the two const VERSION = 'B5.4' declarations inside the PDF exporters. The rest of the export payload — the gb-bizcalc-export-v1 format tag, the __exportedAt__ ISO timestamp, the keys map of every gbBizCalc* + tc_* localStorage key, the JSON pretty-printing, and the new B5.3 #1 daily-letter filename advance on import — is unchanged. The import-side validator (obj.__format__ !== 'gb-bizcalc-export-v1') is also unchanged so older exports stamped 'B0.8' still restore cleanly — only the displayed version string in new exports advances. NOTE: this fix does NOT touch the dozens of historical B0.x / B1.x / B2.x markers inside CSS / JS source comments (e.g. /* B0.8 #1: GB-CALC button styled... */) — those are intentional historical references documenting which prior release a particular code block traces to and they correctly stay frozen. (2) VERSION STAMP. Bumped from B5.3 -> B5.4 throughout: title bar (GB-BizCalc - B5.4), User Guide stamps, version-badge, all PDF headers/footers, every save-as filename (every -B5.3.pdf -> -B5.4.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.4" news PDF footer string, internal VERSION constants updated to 'B5.4', AND the JSON export __version__ field per #1 above (now reads 'B5.4' in every new gb-bcalc-MM-DD-YY[a-z].json the app downloads). The JSON daily-letter naming convention from B4.9 #2 is unchanged — still emits gb-bcalc-MM-DD-YY[a-z].json with the B5.3 #1 post-load advance applied. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card, the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, the B5.2 #2 right-click timeline-node delete with favorites preservation, the B5.3 #1 post-load letter-counter advance, the B5.3 #2 right-click news-card delete with favorites preservation, and the B5.3 #3 modal-head star are all byte-for-byte identical to B5.3 except for the one field above plus the version stamp. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.3, B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.3Apr 2026Three changes on top of B5.2 plus the version bump. (1) JSON LOAD ADVANCES THE LETTER INDEX. The B4.9 #2 daily filename convention (gb-bcalc-MM-DD-YY[a-z].json) used to overwrite itself after a Load: if the user loaded gb-bcalc-04-29-26g.json from Downloads, the local letter counter (gbBizCalcJsonExportLetter_v1) was overwritten by the value baked into the FILE — typically the same idx as the file's own letter — so the very next Save also produced gb-bcalc-04-29-26g.json and the browser silently overwrote or renamed the existing download. importAllAppData() now parses the loaded file's name with /^gb-bcalc-(\d{2}-\d{2}-\d{2})([a-z]+)\.json$/i, decodes the bijective base-26 letter back to its numeric idx, and (when the file's date matches today) sets the local counter to MAX(current value, parsed letter idx). The result: loading gb-bcalc-04-29-26g.json on 04/29/26 forces the next Save to advance to gb-bcalc-04-29-26h.json. MAX-not-overwrite means loading a STALE file (e.g. an earlier same-day save) doesn't roll the counter backward. Files with a different date are ignored so cross-day loads keep today's clean "a" start. The rest of the bijective base-26 encoder, the per-day reset on date rollover, and the import flow itself (clear-then-restore + reload) are unchanged. (2) RIGHT-CLICK NEWS ARTICLE → DELETE THIS ARTICLE. Each card in the Tariff News grid (the thumbnail view that shows BEFORE you click an article to read it in full view) now has oncontextmenu wired up. Right-click any card opens a one-row context menu reading "Delete This Article?" pinned at the cursor; clicking that row hides the menu and opens a center-screen native window.confirm("CONFIRM ARTICLE DELETION") dialog (the same iOS-style alert pattern as the B5.1 #1 tariff-history confirm and the B5.2 #2 timeline-node confirm). On OK we delete the article from newsItems[] and persist the new array back to localStorage at NEWS_CACHE_KEY = 'tc_news_v3'. EXCEPTION: if the article is currently favorited (i.e. its id is in newsFavorites / NEWS_FAVORITES_KEY = 'tc_news_favorites_v1'), we keep the article in newsItems[] but flag it with __deletedFromAll = true so the FAVORITES view continues to show it while the default all-articles grid hides it. renderNews() was extended to filter out __deletedFromAll items in the all-mode path; the favorites-mode path is unchanged so favorited copies stay visible there. The cache write preserves the existing 'ts' field so the freshness gate (NEWS_CACHE_TTL = 90 days) is unchanged. Click-outside / Esc dismiss the new context menu the same way the GB - TARIFF HISTORY and timeline-node menus do. (3) STAR BUTTON IN OPEN-ARTICLE MODAL HEAD. The news article modal (the popup that opens when you click an article to read it in full view) now shows a star button immediately to the LEFT of the X close button at the top right. Visual contract matches the news-card star: gray default (#bdbdbd), bright yellow when starred (#fdd835), with the same drop-shadow glow and hover lift. Clicking it toggles the open article's favorite state — it turns yellow if it was gray, gray if it was yellow — by delegating to the shared toggleFavorite() so the underlying card star, the favorites Set, and the modal star all update together. openNewsModal() syncs the modal star's class to the article's current favorite state every time the modal opens. The modal-head flex layout was updated by wrapping the new star + the existing X in a small gap:10px flex group on the right so justify-content:space-between still pushes them as one block opposite the title. (4) VERSION STAMP. Bumped from B5.2 -> B5.3 throughout: title bar (GB-BizCalc - B5.3), User Guide stamps, version-badge, all PDF headers/footers (including the B4.9 #1 ALL COST BASIS footer), every save-as filename (every -B5.2.pdf -> -B5.3.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.3" news PDF footer string, and internal VERSION constants updated to 'B5.3'. The JSON daily-letter naming convention from B4.9 #2 is unchanged — it still emits gb-bcalc-MM-DD-YY[a-z].json with no version stamp in the filename (now with the post-load advance from #1 above). NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering on Customer Cards and Macro Cost Basis, sub-profile drag-drop within a card, the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature, the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention (now with the B5.3 #1 post-load advance), the B5.1 #1 tariff-history center-screen confirm, the B5.1 #2 black ship-col-head, the B5.2 #1 multi-store tariff-history delete fix, and the B5.2 #2 right-click timeline-node delete with favorites preservation are all byte-for-byte identical to B5.2 except for the three areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.2, B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.2Apr 2026Two changes on top of B5.1 plus the version bump. (1) GB - TARIFF HISTORY DELETE BUG FIX. The right-click "DELETE THIS LOG" → "CONFIRM LOG DELETE" flow used to require the user to repeat the entire delete-and-confirm sequence twice for an entry to actually disappear: clicking CONFIRM LOG DELETE the first time only seemed to do nothing, and a second right-click + delete + confirm was needed before the entry left the list. Root cause: ugThDeleteEntry() resolved the entry through ugThFindEntry() which returns the FIRST matching store and stops. When the modal was rendering the merged US:CN view (per-route store gbTariffGuideHistory_US_CN PLUS the legacy global store gbTariffGuideHistory), an entry that legitimately existed in both stores would only be spliced from one of them; the next render then re-merged the surviving copy back into the displayed list. The fix walks every candidate store (active route store + legacy global store) and removes EVERY matching ts from each, persisting only the stores that actually changed. The right-click first-step menu (DELETE THIS LOG / DELETE ALL THESE LOGS) is unchanged, the center-screen window.confirm() text is unchanged (CONFIRM LOG DELETE / CONFIRM DELETE ALL THESE LOGS), only the splice routine is broader. ugThDeleteAllEntries() already cleared both stores when the active route was US:CN so its behavior is unchanged. (2) RIGHT-CLICK TIMELINE NODE → DELETE THIS TIMELINE HISTORY NODE. In the Tariff News horizontal timeline slider (B5.1 #1 — the slider that scrubs through historical news batches), the user can now right-click the slider thumb (or anywhere on the slider track) to open a one-row context menu reading "DELETE THIS TIMELINE HISTORY NODE". Clicking that row hides the menu and opens a center-screen native window.confirm("CONFIRM NODE DELETE") dialog (the same iOS-style alert pattern used by the B5.1 #1 tariff-history confirm). On OK we delete every cached news article whose __fetchedAt timestamp matches the parked-on batch from both newsItems[] and the localStorage cache (NEWS_CACHE_KEY = 'tc_news_v3'). Sliding the timeline after the delete no longer stops on that node — the batch is gone from the slider order, and none of its articles appear in the news grid. FAVORITES are preserved: any article from the deleted batch that was already starred (i.e. its id is in newsFavorites / NEWS_FAVORITES_KEY = 'tc_news_favorites_v1') is kept in newsItems with its __fetchedAt re-stamped to 0 so the surviving article still appears in the FAVORITES view but no longer anchors a timeline tick at the deleted batch (it sorts to the right-end "earlier" tick alongside other pre-batch items). The cache write preserves the existing 'ts' field so the page-load freshness gate (NEWS_CACHE_TTL = 90 days) is unchanged. Click-outside / Esc dismiss the new context menu the same way the GB - TARIFF HISTORY menu does. (3) VERSION STAMP. Bumped from B5.1 -> B5.2 throughout: title bar (GB-BizCalc - B5.2), User Guide stamps, version-badge, all PDF headers/footers (including the B4.9 #1 ALL COST BASIS footer), every save-as filename (every -B5.1.pdf -> -B5.2.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.2" news PDF footer string, and internal VERSION constants updated to 'B5.2'. The JSON daily-letter naming convention from B4.9 #2 is unchanged — it still emits gb-bcalc-MM-DD-YY[a-z].json with no version stamp in the filename. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering on Customer Cards and Macro Cost Basis, sub-profile drag-drop within a card (locked-target guard from B4.7 still in place), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature (right-click LOCK PROFILE / mini-padlock indicator / drag-drop block / read-only inputs / DELETE CUST. CARD UNLOCK gate / 2-row chart heading wrap / no-Say-Hi-to-Julie row), the B4.9 #1 PDF - ALL COST BASIS combined export, the B4.9 #2 JSON filename convention, the B5.1 #1 tariff-history center-screen confirm, and the B5.1 #2 black ship-col-head all remain byte-for-byte identical to B5.1 except for the two areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B5.1, B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B5.1Apr 2026VERSION-NUMBERING CORRECTION: this build was first stamped "B4.10" in error. Per the GB-BizCalc Version Numbering System the minor digit is single-digit only — "B4.10" is illegal — so it has been corrected to B5.1 throughout the app, every PDF download, and the HTML filename. Treat the work below as if it were always stamped B5.1; B5.0 was skipped to make the correction unambiguous in the historical changelog. Two changes on top of B4.9 plus the version bump (see VERSION-NUMBERING CORRECTION preamble above). (1) GB - TARIFF HISTORY DELETE CONFIRMS go center-screen. The right-click menu inside any "9. Each Tariff Explained" GB - TARIFF HISTORY entry header still shows the same two options (DELETE ALL THESE LOGS / DELETE THIS LOG) at the cursor. The change is in step 2: clicking either option used to reveal a floating "CONFIRM LOG DELETE" / "CONFIRM DELETE ALL THESE LOGS" button positioned ABOVE the cursor with an 8px gap; that floating second-step is now replaced by a center-screen native window.confirm() dialog (the same iOS-style Cancel | OK alert used by the B4.7 #2 Customer Card delete and the B4.4 #1 chart custom-col delete). The wording stayed identical — "CONFIRM LOG DELETE" for a single-log delete and "CONFIRM DELETE ALL THESE LOGS" for the all-logs delete — only the surface flipped from floating-above to center-screen. The first-step menu still hides on outside click + Esc as before; ugThEntryCtxAction was simplified from a 3-phase state machine (choose / confirmDelete / confirmDeleteAll) to a one-shot dispatcher that calls window.confirm() and routes to ugThDeleteEntry / ugThDeleteAllEntries on OK. (2) PRE-PAID SHIP-CALC HEADINGS turn black. The 3-line column header ("GripBlades / Adjusted / Margin") painted via .ship-col-head above each pre-paid Ship-Calc row group used to render in #8e8e93 gray (var(--text3)). It is now var(--text) #1c1c1e black, matching the value rows underneath. Single CSS swap — color: var(--text3) -> color: var(--text). Affects both column-header placements (the GRIPBLADES MARGIN VS DIST. PRICE section and the GRIPBLADES ROI FROM MSRP DIRECT-TO-END-USER section). No layout change; just darker headings. (3) VERSION STAMP. Bumped from B4.9 -> B5.1 throughout: title bar (GB-BizCalc - B5.1), User Guide stamps, version-badge, all PDF headers/footers (including the B4.9 #1 ALL COST BASIS footer), every save-as filename (every -B4.9.pdf -> -B5.1.pdf, and the JSON naming convention from B4.9 #2 still emits gb-bcalc-MM-DD-YY[a-z].json with no version stamp in the filename), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B5.1" news PDF footer string, and internal VERSION constants updated to 'B5.1'. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering on Customer Cards and Macro Cost Basis, sub-profile drag-drop within a card (locked-target guard from B4.7 still in place), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul (only the right-click confirm flow changed this time — the per-entry PDF + favorite + reorder behavior is unchanged), the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, the entire B4.7 lock feature (right-click LOCK PROFILE / mini-padlock indicator / drag-drop block / read-only inputs / DELETE CUST. CARD UNLOCK gate / 2-row chart heading wrap / no-Say-Hi-to-Julie row), the B4.9 #1 PDF - ALL COST BASIS combined export, and the B4.9 #2 JSON filename convention are all byte-for-byte identical to B4.9 except for the two areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B4.9, B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.9Apr 2026Three changes on top of B4.7 (B4.8 number skipped because the post-B4.7 drag-drop hotfix did not bring its own version stamp). (1) PDF - ALL COST BASIS. The Customer Card right-click menu now has a THIRD row at the bottom (under DELETE CUST. CARD and DUPLICATE CARD) titled "PDF - ALL COST BASIS". The row is styled to match the orange-with-white-text-and-rounded-edges PDF button family used throughout the ROI-Calc tab (background #F4834F, color #fff, border-radius:5px, hover #e0702e). Clicking it generates ONE merged multi-page PDF containing every cost basis (the 3 built-ins + every custom Macro Cost Basis) for the right-clicked Customer Card's currently active Profile. The architecture: exportRoiBasisPDF(basis, variant) was refactored to accept an optional 3rd argument _sharedCtx = { doc, addPageFirst }; when provided the function reuses the shared jsPDF doc, optionally calls doc.addPage() before painting, and short-circuits before its footer + save loop. A new wrapper exportRoiAllBasisCardPDF(cardId) selects the right-clicked card, builds one shared jsPDF, iterates ROI_BASES calling exportRoiBasisPDF with addPageFirst:true on every basis after the first, paints a unified footer ("ROI-Calc B4.9 · cardName · Profile X · ALL COST BASIS") across every page, and saves once as "roi-{safeName}-profile{subId}-allbasis-B4.9.pdf". The legacy per-basis orange PDF buttons in each Macro Cost Basis row continue to pass _sharedCtx === undefined and behave exactly as before (single-basis PDF named "roi-{name}-profile{N}-{basis}-B4.9.pdf"). The pdfAllBasis branch in roiCustomerCtxAction sits right after the duplicate branch in the existing dispatcher. (2) JSON FILENAME CONVENTION. The .json export download (Save Backup / Export All App Data) was producing names like "gb-bizcalc-data-2026-04-29_14-37-12.json". New format is "gb-bcalc-MM-DD-YY[letters].json" where the trailing letters are a per-day bijective base-26 index that increments on every same-day download (a, b, c, …, z, then aa, ab, …) and resets automatically when the local date rolls over. Today's 1st download is gb-bcalc-04-29-26a.json, the 2nd is gb-bcalc-04-29-26b.json, the 3rd is gb-bcalc-04-29-26c.json, etc. State is persisted in localStorage at gbBizCalcJsonExportLetter_v1 as JSON {date:'MM-DD-YY', idx:Number}; if the stored date does not equal today the index resets to 0 (-> 'a'). The bijective base-26 helper handles the roll past z without producing 'ba'-alike gaps so the daily sort order in Finder/Downloads is monotone. The import side (__format__:'gb-bizcalc-export-v1' payload validator) is unchanged so older "gb-bizcalc-data-….json" backups from B4.7 and earlier still restore cleanly. (3) VERSION STAMP. Bumped from B4.7 -> B4.9 throughout: title bar (GB-BizCalc - B4.9), User Guide stamps, version-badge, all PDF headers/footers (including the new ALL COST BASIS footer added in #1), every save-as filename (every -B4.7.pdf -> -B4.9.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B4.9" news PDF footer string, and internal VERSION constants updated to 'B4.9'. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering on Customer Cards and Macro Cost Basis, sub-profile drag-drop within a card (locked-target guard from B4.7 still in place), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes (still gated by B4.7 #5's UNLOCK pre-check), the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, the B4.6 #3 symmetric 18/18 heading padding, and the entire B4.7 lock feature (right-click LOCK PROFILE / mini-padlock indicator / drag-drop block / read-only inputs / DELETE CUST. CARD UNLOCK gate / 2-row chart heading wrap / no-Say-Hi-to-Julie row) are all byte-for-byte identical to B4.7 except for the three areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B4.7, B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.7Apr 2026Eight changes on top of B4.6, organized around a new per-(Customer Card × Profile) LOCK state that protects saved data from accidental overwrite. (1) RIGHT-CLICK A PROFILE LETTER (A-E) IN A CUSTOMER CARD AND THE FIRST OPTION UNDER THE HORIZONTAL COLOR BAR IS NOW "LOCK PROFILE X" (above CLEAR PROFILE / RESET COLOR). Click it and the Profile is locked. Right-click again on a locked Profile and that same row flips to "UNLOCK PROFILE X". The state is keyed at gbBizCalcRoiSubLocked_v1 and mirrors the existing gbBizCalcRoiSubColors_v1 storage shape: Customer Card id → Sub Profile id → boolean. Helpers are roiGetSubLocked / roiSetSubLocked / roiToggleSubLocked / roiCardHasAnyLocked, all loaded on boot alongside roiSubColorsLoad(). (2) WHILE A PROFILE IS LOCKED, ITS HAS-DATA BLUE DOT (the "A •" indicator next to the letter on the Customer Card sub-button) IS REPLACED BY A MINI PADLOCK SVG. CSS rule .roi-card-sub-btn.has-data.is-locked::after{display:none} suppresses the dot, and the rendered button now appends a 10×10 lock SVG inside it that inherits currentColor so the lock visually flips between accent (idle), white (active), and white (custom-color) with no extra rules. (3) DRAGGING ANOTHER PROFILE (or a CLIPBOARD slot) ONTO A LOCKED PROFILE NO LONGER OVERWRITES IT. roiSubDrop early-returns when the target is locked, and roiSubDragOver no longer applies the .roi-sub-drop-hover affordance to locked targets so the user gets no false "ok to drop here" cue. The drag is silently cancelled exactly as if the user dropped on dead space. (4) WHILE A PROFILE IS LOCKED, EVERY INPUT THAT WRITES INTO IT BECOMES READ-ONLY. roiUpdateInputsDisabled now also checks roiGetSubLocked(roiActiveCard, roiActiveSub) and flips disabled=true on (a) every #roiGrid input[data-sku] in the active 10/10MC/40/40MC pack panel, (b) every .roi-promo-btn in the same grid, (c) every .roi-basis-landed-input inside the MACRO COST BASIS bar (the Landed $/pc field per basis option), and (d) every .roi-basis-log-btn (the Log button next to it). Hover titles read "Profile is LOCKED — right-click the Profile letter and choose UNLOCK PROFILE to edit." Every profile switch + basis switch + grid rebuild already calls roiUpdateInputsDisabled, and roiSyncBasisLandedInputs was extended to also call it so freshly-rendered basis options pick up the locked state immediately. (5) RIGHT-CLICK → DELETE CUST. CARD IS BLOCKED WHILE ANY OF THE CARD'S 5 PROFILES IS LOCKED. Instead of the iOS-style window.confirm('CONFIRM DELETE?') dialog, a window.alert('UNLOCK ALL PROFILES TO DELETE CUSTOMER CARD') fires. Every locked profile must be unlocked via right-click → UNLOCK PROFILE before the standard delete flow becomes reachable. roiDeleteCustomerCard also scrubs any orphan lock entries for the doomed card so the gbBizCalcRoiSubLocked_v1 store does not accumulate stale keys. (6) MFR/LANDED COST REF. CHART CUSTOM-COL HEADINGS NOW WRAP TO A SECOND ROW WHEN THE FIRST ROW FILLS. The heading control was switched from <input> to <textarea rows=2> with text-align:center preserved so both lines visually center. The 28-char hard cap is gone — the real cap is "fits inside the locked viewport's 2-line height." Every keystroke is checked against the live textarea's scrollHeight vs. clientHeight, and any keystroke whose resulting wrapped text would push past the visible band is reverted to the last accepted value (with the caret rolled back one position). The CSS rules for is-locked / is-unlocked were extended to also match the new textarea selectors, and resize:none + overflow:hidden + line-height:1.15 + height:2.4em keep the heading visually identical while allowing the wrapped second line. (7) THE "SAY HI TO JULIE" ROW IN THE PROFILE RIGHT-CLICK MENU IS REMOVED, along with the showSayHiJuliePopup() function and its A9.8 #10 toast logic. The remaining menu order (top to bottom under the horizontal color bar) is: LOCK PROFILE / UNLOCK PROFILE, CLEAR PROFILE, RESET COLOR. (8) VERSION STAMP. Bumped from B4.6 -> B4.7 throughout: title bar (GB-BizCalc - B4.7), User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B4.6.pdf -> -B4.7.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B4.7" news PDF footer string, and internal VERSION constants updated to 'B4.7'. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering on Customer Cards and Macro Cost Basis, sub-profile drag-drop within a card (B4.7 #3 only adds the locked-target guard), the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes (B4.7 #5 only adds a pre-confirm UNLOCK gate), the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings, the B4.6 #1 wrapped tariff-PDF gold notes / inactive name, the B4.6 #2 document-level changelog-row PDF delegate, and the B4.6 #3 symmetric 18/18 heading padding are all byte-for-byte identical to B4.6 except for the eight areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B4.6, B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.6Apr 2026Three fixes on top of B4.5. (1) TARIFF-ANALYSIS PDF DOWNLOAD: GOLD STATUS-NOTE LINES NO LONGER RUN OFF THE RIGHT EDGE OF THE PAGE. Inside exportPDF the NON-ACTIVE TARIFFS — Reference Only block emitted each entry's gold (setTextColor 180,130,80) status_note via raw doc.text(t.status_note, M, y) with no width cap, so any note longer than the printable area (W-2*M ~= 512pt) ran past the page margin. Wrapped the note through doc.splitTextToSize(t.status_note, W-2*M) and emit each line at +12pt y so multi-line notes flow naturally within the printable column. Also wrapped the inactive-tariff NAME line on the same row — the bold gray name+rate pair previously had no width cap either, so a long inactive tariff name could collide with the right-aligned rate label ("100% (inactive)"). Now the name is split via splitTextToSize against W-2*M-110pt (110pt reserved on the right for the rate label) and any extra lines flow at +12pt y in the same bold gray style; the rate label still sits on the first baseline at right. Active-tariff name + description wrapping (B2.5 #6 / #8 /#9) is unchanged — it was already correct. The (180,130,80) gold colour and 8pt size of the status-note line are preserved so visual continuity with prior PDFs is intact. (2) PER-CHANGELOG-ROW PDF BUTTONS NOW FIRE FROM BOTH THE COMPACT AND FULL-SCREEN VIEWS. The small orange "PDF" pill (.ug-cl-row-pdf-btn, B1.9 #4) sitting between each row's version label and date worked in the compact User Guide changelog but did NOTHING when clicked in the full-screen overlay opened by the BIG button. Root cause: ugOpenChangelogFull() copies the rows into the big view via #ugChangelogFullList.innerHTML = #changelogScroll.innerHTML — innerHTML cloning preserves attributes (including data-cl-ver) but strips every addEventListener wiring, so the cloned buttons lived in the DOM with no click handler at all. Fix: replaced the per-button btn.addEventListener inside ugInstallPerVersionPdfButtons with a SINGLE document-level delegated listener installed by a new ugInstallChangelogRowPdfDelegate() that runs on DOMContentLoaded. The delegate listens at document scope, walks ev.target.closest('.ug-cl-row-pdf-btn'), and calls ugExportSingleChangelogRowPDF with the data-cl-ver value — which survives the innerHTML clone intact. A window._ugClRowPdfDelegateInstalled guard prevents duplicate handlers if the function ever runs twice. Result: clicking the per-row PDF pill in EITHER view now reliably downloads the gb-bizcalc-changelog-{VER}-B4.6.pdf for that row. The master "CHANGELOG — FULL HISTORY" PDF pill on the big-view header (which has its own onclick="..." attribute) was never affected and continues to work as before. (3) CUSTOM-COL HEADING TEXT IS NOW TRUE-CENTERED ACROSS THE CELL, NOT OFFSET TO THE RIGHT. The B4.5 #1 is-locked / is-unlocked input rules used asymmetric padding (padding-left:18px / padding-right:4px) to leave room for the absolutely-positioned padlock at top-left. Side-effect: text-align:center then centered the text between the padded edges, which placed the heading visibly to the right of true cell center. Symmetrized the padding to 18px / 18px on BOTH the is-locked and is-unlocked input rules so the centered text now sits at the true horizontal midpoint of the cell, exactly as if the padlock isn't there — matching the user's reading expectation. The padlock's own absolute position (top:2px left:3px width/height 16px) is unchanged, so it still lives in the top-left corner and overlaps the left-padding region without colliding with the typed heading. (4) VERSION STAMP. Bumped from B4.5 -> B4.6 throughout: title bar (GB-BizCalc - B4.6), User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B4.5.pdf -> -B4.6.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B4.6" news PDF footer string, and internal VERSION constants updated to 'B4.6'. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering on Customer Cards and Macro Cost Basis, sub-profile drag-drop within a card, the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, the B4.4 #2 forced-white built-in heading rule, and the B4.5 #1 padlock + black-while-typing / white-when-locked custom-col headings are all byte-for-byte identical to B4.5 except for the three areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B4.5, B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.5Apr 2026Two changes on top of B4.4. (1) MFR / LANDED COST REF. CHART CUSTOM-COLUMN HEADINGS NOW HAVE A TINY PADLOCK + BLACK-WHILE-TYPING / WHITE-WHEN-LOCKED VISUAL TREATMENT. Each user-added column heading is now rendered as an absolutely-positioned 16x16 padlock button at the top-left of the heading cell (.mfr-cost-ref-customlock) plus the heading input below it. The padlock reuses the existing 12x12 ROI_LOCK_SVG_LOCKED / ROI_LOCK_SVG_OPEN icons used by the Customer Card / Macro Cost Basis padlocks so the visual language matches across the app. The custom-col data model gained a `locked` boolean (persisted to localStorage at gbBizCalcMfrCostRefCustomCols_v1 / gbBizCalcLandedCostRefCustomCols_v1 inside each col entry alongside `key` and `label`). LOCKED state: input is read-only, white bold uppercase centered text on the transparent dark green (MFR, #2d6a4f) / dark blue (LANDED, #003e8a) header band, no visible border, default cursor — matches the surrounding built-in heading band so the locked state reads as part of the dark unified header row. UNLOCKED state: input is editable, BLACK bold uppercase centered text on a solid white background with a 1px black border (focus adds a subtle white halo) so the user can clearly see what they are typing. Click the padlock icon to toggle: an unlocked column is auto-focused with its existing text pre-selected so the user can type over it; a locked column blocks all keystrokes. Click outside an unlocked heading (input.blur) auto-locks the column so the user cannot accidentally erase a heading by tabbing through. Brand-new columns added via the "+" button start UNLOCKED so the user can type the heading immediately (then auto-lock on blur). Existing custom columns persisted before B4.5 do NOT have a `locked` field — those default to LOCKED for safety so a previously-typed heading is protected against accidental edits when the file is first opened in B4.5. The mousedown.preventDefault wiring on the padlock button keeps the input from firing an extra blur event between click and re-render (which would auto-lock and reverse the desired toggle). The original 3 base columns of each chart are unchanged — they still render as plain white-text divs with no padlock, since their headings are app-defined and never editable by the user. Right-click DELETE THIS COL. -> native window.confirm('CONFIRM DELETE?') on custom cols (B4.4 #1) is unchanged. The B4.4 #2 forced-white blanket rule for built-in headings is also unchanged — it now coexists with two new, higher-specificity rules that flip the custom-col input white-on-transparent (locked) or black-on-white (unlocked) depending on which class the parent .mfr-cost-ref-customhead carries. (2) VERSION STAMP. Bumped from B4.4 -> B4.5 throughout: title bar (GB-BizCalc - B4.5), User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B4.4.pdf -> -B4.5.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B4.5" news PDF footer string, and internal VERSION constants updated to 'B4.5'. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering on Customer Cards and Macro Cost Basis, sub-profile drag-drop within a card, the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes, the B4.4 #1 native-confirm dialog for chart custom-col deletes, and the B4.4 #2 forced-white built-in heading rule are all byte-for-byte identical to B4.4 except for the area above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B4.4, B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.4Apr 2026Three small fixes on top of B4.3. (1) MFR / LANDED COST REF. CHART NEW-COLUMN DELETE NOW USES THE NATIVE CENTER-SCREEN BROWSER ALERT. Right-clicking a user-added column heading and choosing "DELETE THIS COL." previously opened a floating second-step menu that repositioned 8px above the cursor with a single red "CONFIRM COL. DELETE" button (the B0.3 #3 / B4.3 #1 confirm-above pattern). User feedback: a center-screen confirm dialog feels safer and more consistent with the other delete prompts now in the app. Replaced the flow inside mfrChartColCtxAction: clicking DELETE THIS COL. now hides the right-click menu and immediately calls window.confirm('CONFIRM DELETE?'), which renders as the standard iOS-style center-screen modal with white background, rounded corners, light divider, and blue Cancel | OK buttons — same dialog used by the Cost Per Click log delete, the B4.3 Customer Card DELETE CUST. CARD, and the B4.3 Macro Cost Basis DELETE PROFILE. Cancel aborts; OK fires mfrCostRefDeleteColumn exactly as before. The first-step right-click menu (DELETE THIS COL. on top) is unchanged. Original 3 base columns of each chart still never open this menu and cannot be deleted. The 'confirm' phase branch is kept as a defensive no-op in case any cached menu state lands there. The mirroring tariff-history per-entry/all-entries delete from B4.1 #3c still uses the cursor-anchored confirm-above pattern — those operate on tightly scoped child data and the inline confirm fits their flow better. (2) NEW-COLUMN HEADING TEXT NOW WHITE, CENTERED, BOLD UPPERCASE. The user-typed name in a new MFR / LANDED chart column was rendering as small dim dark gray text right-aligned against the dark green (MFR, #2d6a4f) / dark blue (LANDED, #003e8a) header band — almost invisible. Cause: .mfr-cost-ref-customhead input had explicit white/centered/bold styling but the more general .mfr-cost-ref-cell input rule lives LATER in the stylesheet at equal class-specificity, so by source order it was overriding color (var(--text2) dark gray), text-align (right), font-size (11px), and font-weight (400). Added a higher-specificity override #mfrCostRefGrid .mfr-cost-ref-customhead input + #landedCostRefGrid .mfr-cost-ref-customhead input that forces color:#fff !important, text-align:center !important, font-family:var(--font), font-size:12px, font-weight:700, letter-spacing:.03em, text-transform:uppercase, plus matching ::placeholder rules. Also added an explicit blanket rule #mfrCostRefGrid .mfr-cost-ref-colhead, #mfrCostRefGrid .mfr-cost-ref-colhead input, #landedCostRefGrid .mfr-cost-ref-colhead, #landedCostRefGrid .mfr-cost-ref-colhead input{color:#fff !important} so EVERY heading cell in either chart (built-in or user-added, label text or input text) is guaranteed white against the dark band regardless of which specificity rule fires. The dashed rgba(255,255,255,.35) border around the input that hints "clickable to rename" is unchanged. (3) VERSION STAMP. Bumped from B4.3 -> B4.4 throughout: title bar (GB-BizCalc - B4.4), User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B4.3.pdf -> -B4.4.pdf), Save-a-copy HTML download, the "GB-BizCalc TariffNews · B4.4" news PDF footer string, and internal VERSION constants updated to 'B4.4'. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card, the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, the B4.1 #3 Tariff History overhaul, the B4.3 #1 WAS-prefix-black drop annotation, and the B4.3 #2 native-confirm dialog for Customer Card / Macro Cost Basis deletes are all byte-for-byte identical to B4.3 except for the two areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B4.3, B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.3Apr 2026Three small fixes on top of B4.1.1, plus a corrective version stamp (the previous patch was numbered B4.1.1 but should have been B4.2 — going straight to B4.3 here so the version stream resyncs to a normal two-component minor cadence). (1) PRE-PAID SHIP-CALC "WAS" PREFIX NOW BLACK. The drop-annotation line under each distributor row ("WAS - $208.68 / 31.68% · drop -$25.00 / -3.79%") had the entire line rendering as the secondary-annotation gray var(--text3) — making it hard to spot the "WAS" anchor at a glance. Wrapped the leading "WAS" substring in a small span (.ship-row-drop-was, color:#000, font-weight:700) so it now renders in solid black bold against the gray rest of the line. The dollar/percent values, the "drop" label, and the negative drop amount all stay var(--text3) — only the WAS prefix changed. Applied to all four distributor tier rows (A-DIST / B-DIST / C-DIST / D-DIST) for both pack sizes. (2) DELETE CONFIRMATION DIALOGS NOW USE THE NATIVE CENTER-SCREEN BROWSER ALERT. CUSTOMER CARD right-click "DELETE CUST. CARD" and MACRO COST BASIS right-click "DELETE PROFILE" previously used a floating second-step menu that repositioned 8px above the cursor with a single red "CONFIRM DELETE" button (the B0.3 #3 / B1.0 #3 confirm-above pattern). User feedback: a center-screen confirm dialog feels safer and more consistent with the existing "Permanently delete the entire Cost Per Click log?" prompt that uses the native browser confirm(). Replaced both flows: clicking DELETE CUST. CARD or DELETE PROFILE now hides the right-click menu and immediately calls window.confirm('CONFIRM DELETE?'), which renders as the standard iOS-style center-screen modal with white background, rounded corners, light divider, and blue Cancel | OK buttons. Cancel aborts the delete; OK fires roiDeleteCustomerCard / roiDeleteCustomBasis exactly as before. The first-step right-click menu (DELETE on top, DUPLICATE under cursor) is unchanged so DUPLICATE is still the easy in-place click and DELETE still requires deliberately moving up to the top option. The 'confirm' phase branch in both ctx-action handlers is kept as a defensive no-op in case any cached menu state lands there. Right-click DELETE menus that still use the cursor-anchored confirm-above pattern (cost-ref chart custom column delete from B4.1, tariff-history per-entry/all-entries delete from B4.1 #3c) are unchanged — those operate on tightly-scoped child data and the inline confirm fits their flow better. (3) VERSION STAMP CORRECTION. Bumped from B4.1.1 -> B4.3 throughout: title bar (GB-BizCalc - B4.3), User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B4.1.1.pdf -> -B4.3.pdf), Save-a-copy HTML download, and the literal "GB-BizCalc TariffNews · B4.1.1" news PDF footer string introduced in B4.1.1 #1. Internal VERSION constants updated to 'B4.3'. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card, the B4.1 cost-reference-chart custom column system, the B4.1 #2 Sea Freight Method PDF, and the B4.1 #3 Tariff History overhaul are all byte-for-byte identical to B4.1.1 except for the three areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. The B4.1.1, B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record. (B4.2 was skipped per user direction.)
B4.1.1Apr 2026Patch on top of B4.1. Three small fixes. (1) TARIFFNEWS PER-ARTICLE PDF FOOTER NOW SHOWS THE VERSION. The footer line in exportNewsArticlePDF was reading (typeof VERSION === 'string' ? VERSION : '') but the local const VERSION is declared inside other functions (exportCostHistoryPdf, exportRoiPDF) and is NOT in scope here, so the ternary always picked the empty-string fallback — every downloaded news article PDF rendered the bottom-left footer as "GB-BizCalc TariffNews · " with nothing after the dot. Replaced with a literal version string "GB-BizCalc TariffNews · B4.1.1" so every news article PDF footer now correctly stamps the build that produced it. The per-release version bumper handles rolling that literal forward. (2) VERTICAL SCROLL LOCK REGRESSION FIXED. Symptom: working inside any calculator panel (Tariff-Calc, MFR-CostCalc, ROI-Calc, Pre-Paid Ship-Calc) the page would sometimes refuse to scroll vertically; the only reliable workaround was to open the User Guide, click around, and close it again — after which scrolling worked. Cause: document.body.style.overflow was being set to "hidden" by ugOpenChangelogFull, ugOpenTariffHistoryRoute, and ugOpenTariffHistory (the global variant) to keep their full-screen overlays on top of any User Guide/Changelog stacks. The release path in ugCloseTariffHistory only restored body.overflow to empty if NEITHER #howBg NOR #ugChangelogFullBg was still "on". But the User Guide modal does NOT actually need body locked — it has its own internal overflow-y:auto scroll container — and closeHowDirect (the User Guide's close path) never touched body.overflow. So a flow like [open User Guide] -> [open Tariff History from inside it] -> [close Tariff History] left body.overflow stuck at "hidden" (because howBg was still on, the stillLocked check kept the lock), and then [close User Guide] dropped #howBg without releasing the lock. Body scroll stayed disabled until some other overlay flow happened to clear it. Refactor: introduced _appBodyScrollSync(), a single source of truth that re-derives the correct lock state from only the modals that ACTUALLY require it (#ugChangelogFullBg and #ugTariffHistoryBg). Every modal open/close path now calls _appBodyScrollSync() instead of writing body.style.overflow directly: ugOpenChangelogFull, ugCloseChangelogFull, ugOpenTariffHistoryRoute, ugOpenTariffHistory, ugCloseTariffHistory, and (defensively) closeHowDirect. The helper is also wired to window's "focus" event so a stale lock that survives a tab-away-and-back gets auto-cleared, which mirrors the user observation that switching to another window and scrolling there often unstuck the in-app scroll. The User Guide is no longer treated as a body-locking overlay anywhere, so closing any sequence of nested modals always lands body.overflow in the correct state. (3) TARIFF NEWS HEADING TEXT CLEANED UP. The orange always-visible ticker label that previously read "Tariff-News" (rendered as "TARIFF-NEWS" due to the .ticker-label CSS text-transform:uppercase) was changed to "Tariff News" (renders as "TARIFF NEWS"). The yellow slide-down peek-tab heading was changed from "TariffNews" (one word) to "Tariff News" (two words). Sub-label "Updates & Politics" unchanged. No layout/CSS rules changed — only the visible text. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), Customer Card right-click menu (B1.1 #5), the B4.1 cost-reference-chart custom column system, the B4.1 Sea Freight Method PDF, and the B4.1 Tariff History overhaul are all byte-for-byte identical to B4.1 except for the three areas above. Pricing constants and max_tokens unchanged. Per-click cost unchanged. Version bumped from B4.1 -> B4.1.1 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B4.1.pdf -> -B4.1.1.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B4.1.1'. The B4.1, B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.1Apr 2026Three feature areas added on top of B4.0. (1) MFR COST REF. CHART + LANDED COST REF. CHART now support USER-ADDED COLUMNS via a "+" cell pinned to the right edge of each chart. Clicking "+" appends a fresh data column whose colored heading bar (green for MFR, blue for LANDED) is editable in place — type up to 28 characters and the label persists in localStorage under per-chart keys (mfrCostRefDataKey + "_customCols_v1" and landedCostRefDataKey + "_customCols_v1"). The grid wrapper is now horizontally scrollable (overflow-x:auto on .mfr-cost-ref-scroll) so any number of custom columns can extend off-screen to the right while the row-header column stays locked on the left via position:sticky;left:0 on the corner cell + .mfr-cost-ref-rowhead cells. New rows added by Add Custom Field still cross-populate every column (original 3 + every user-added column) zeroed-out by default; per-cell values are stored in the same per-cell map keyed as rowKey + "-" + colKey so deleting a custom column purges only its own data. RIGHT-CLICK ON A CUSTOM COLUMN reveals a small floating menu ("DELETE THIS COL.") at the cursor; clicking that re-renders the menu 8px ABOVE the cursor with a single bright-red "CONFIRM COL. DELETE" — the deliberate up-and-click motion (mirrors the B0.3 #3 / B4.0 #1 confirm-above pattern) prevents accidental column wipes. The original 3 base columns NEVER receive the contextmenu listener, so their right-click is the browser default and they cannot be deleted. Esc / click-outside dismisses the menu without deleting. (2) PER-SEA-FREIGHT-METHOD PDF DOWNLOAD. The Incoterms detail popup (the modal that opens when you click any FOB / CIF / EXW / DDP / etc. method on the SEA FREIGHT METHOD chart) now has a small orange PDF button (.inco-detail-pdf-btn, #incoDetailPdfBtn) absolutely anchored to right:14px;bottom:14px of the .inco-detail-modal box — same orange/white scheme (#ff8a1f bg, #fff text, 1px #d96f00 border) used by every other PDF download in this app. The .inco-detail-modal element gets position:relative + padding-bottom:60px so long supplier/buyer responsibility lists never slide under the button. New module-level state: let _currentIncotermCode = null; set inside showIncoDetail() and cleared in closeIncoDetailDirect() so a stale code never survives a close-then-reopen cycle. New exportIncotermPDF() builds a letter-format jsPDF document with: dark 62pt header band, white 19pt "Sea Freight Method — CODE" title, gray 10pt timestamp + Incoterms 2010 reference subtitle, 16pt full method name, italic gray category line, then sectioned bold 12pt headings (Overview, Risk Transfer Point, Supplier Responsibilities, Buyer (Importer) Responsibilities, Notes for US Importers) with 11pt bullet bodies that word-wrap with doc.splitTextToSize, hairline rules between sections, italic gray ICC source disclaimer, and a per-page footer ("GB-BizCalc — Sea Freight Method · B4.1" left, "Page X of Y" right). Filename: sea-freight-method-CODE-B4.1.pdf (CODE lowercased) so multi-method downloads sort cleanly. (3) GB-TARIFF HISTORY OVERHAUL. Three sub-changes that touch the per-route history modal (US:CN, CA:MX, MX:US, etc. plus the legacy global guide). (3a) DEDUPE-ON-WRITE: ugSnapshotRouteHistory and ugSnapshotTariffGuide now compare the new innerHTML against the most recent stored entry for that store and skip the unshift entirely when they match. Result: clicking UPDATE TARIFFS twice in a row with no rate change no longer pads the log with a duplicate snapshot. The 50-entry cap is unchanged; freshly-changed snapshots still age out the oldest. (3b) ENTRY-HEAD RESKIN + CLICK-TO-COLLAPSE + PER-ENTRY PDF. Each rendered entry now has id="ugThEntry_TSID" + data-ts="ISO" so the toggle/right-click/PDF handlers can resolve which entry was acted upon. The header bar (.ug-th-entry-head) is now BLACK (#1c1c1e bg, #fff stamp, #bbbcc1 italic note, hover #262628), cursor:pointer, and onclick="ugThToggleEntry(...)" toggles a .collapsed class on the parent entry — a chevron icon (▾ open / ▸ collapsed) on the right edge of the row updates to match. The collapsed state hides .ug-th-entry-body via display:none. The body now reserves padding-bottom:56px for an orange .ug-th-entry-pdf-btn anchored at right:14px;bottom:14px — clicking it calls ugExportTariffHistEntryPDF(ts) which finds the entry in either the active route store or the legacy global store, builds a temporary DOM fragment from the saved html (sans .how-back-top anchors), walks every child node through the shared _ugPdfMake / _ugPdfEmitNode / _ugPdfFoot pipeline, and saves as gb-tariff-history-entry[-route]-SAFESTAMP-B4.1.pdf. The legacy "↑ Back to Tariff Table of Contents" anchor is hidden inside log bodies via .ug-th-entry-body .how-back-top{display:none !important} since the link routes to the User Guide TOC and has no meaningful behavior inside the modal log view. (3c) RIGHT-CLICK DELETE MENU on any header row. New element #ugThEntryCtx (z-index:2147483700, reuses .roi-profile-ctx visual style) renders two stacked options: "DELETE ALL THESE LOGS" (top) and "DELETE THIS LOG" (bottom). Clicking either swaps the menu to a single bright-red CONFIRM button positioned 8px ABOVE the original cursor (same up-and-click pattern as the cost-ref-chart column menu), so a deliberate move + click is required before either delete fires. ugThDeleteEntry(ts) splices the matching entry out of whichever store holds it (per-route OR legacy global) and re-renders. ugThDeleteAllEntries() clears the active store key (and also clears the legacy global store when the user is viewing the merged US:CN view, since that view shows both stores stitched together). The new ctx element lives near #mfrChartColCtx in the body so it is always available when the modal opens; mousedown-outside and Esc both dismiss without deleting. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, cache-first calculate from B3.6 #1, news fetcher (loadNews + B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), Customer Card right-click menu (B1.1 #5), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card, the B3.9 #1-#5 TariffNews FAVORITES + per-article PDF feature set, the B4.0 #2-#3 §9 tariff-popup z-index fix + per-tariff PDF, and every other prior feature are all byte-for-byte identical to B4.0 except for the three areas above. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged. Per-click cost unchanged. Version bump from B4.0 -> B4.1 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers (including the new sea-freight-method PDF and tariff-history-entry PDF), save-as filenames (every -B4.0.pdf -> -B4.1.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B4.1'. The B4.0, B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B4.0Apr 2026Three fixes on top of B3.9. (1) BLANK-TAB REGRESSION FIXED. The B3.9 changelog note included a literal <div> reference (in the prose for the news article PDF parsing description) which the browser HTML parser interpreted as a real opening tag. Because that orphan tag landed inside the User Guide changelog table, every panel that came AFTER the changelog row in the DOM (panelMfr, panelRoi, panelShip) ended up nested inside the dangling open tag. The CSS rule .app-panel{display:none} on a wrapping ancestor effectively hid all non-Tariff tabs. Reproducible test: the .shell wrapper had +1 unbalanced opening div in B3.9 and zero in B3.8. The two literal HTML-tag references in the B3.9 changelog note (<div> and <safe-headline>) are now HTML-escaped (</>) so the browser renders them as visible text instead of parsing them as tags. All four real app tabs (Tariff-Calc, MFR-CostCalc, ROI-Calc, Pre-Paid Ship-Calc) render again. (2) FAVORITES PANEL EMPTY-STATE NO LONGER HIDES THE FAVORITES BUTTON. In B3.9, when the user clicked FAVORITES with zero starred articles, renderNewsTimeline would receive an empty items array and exit at the early-return `if (!items || !items.length) { tl.style.display = "none"; ... return; }`. That hid the entire timeline strip, including the FAVORITES button itself, leaving the user no way to toggle back to all-articles without closing the popup. The early-return is removed: renderNewsTimeline now ALWAYS renders the strip when called, hiding the readout + slider when there are no items but keeping the History label and the FAVORITES button so the user can always click it again to return to the normal news panel. The renderNews empty-state message ("No favorites yet — click the star at the top of any article to add it here.") still shows in the news area below. (3) USER GUIDE §9 TARIFF POPUP NOW PAINTS ON TOP + HAS A PER-TARIFF PDF BUTTON. Two-part fix to the LegalDetail popup that opens when the user clicks any tariff inside User Guide section "9. Each Tariff Explained". (a) Z-INDEX: .modal-bg defaulted to z-index:300 while .how-modal-bg (the User Guide modal) is at z-index:2147483000. Result: the tariff popup opened BEHIND the User Guide and the user had to close the guide to see it. New CSS rule `#legalBg{z-index:2147483600}` lifts the tariff-detail modal above the User Guide modal so it always paints on top. (b) PER-TARIFF PDF: small orange PDF button (.legal-modal-pdf-btn, #legalModalPdfBtn) absolutely anchored to right:14px;bottom:14px of the .modal box — same orange/white scheme (#ff8a1f bg, #fff text, 1px #d96f00 border) used by every other PDF download in this app. The .modal element gets position:relative and #legalBg .modal-body gets padding-bottom:56px so long write-ups never slide under the button. New module-level state: `let currentLegalTariff = null; let currentLegalRouteKey = null;` set in BOTH entry points — openLegal() (Tariff-Calc panel flow, route is null) and ugShowTariffDetailRoute() (User Guide §9 flow, route key is "US:CN" / "CA:MX" / etc.). closeLegalDirect() resets both so a stale tariff never survives a close-then-reopen cycle. New exportTariffDetailPdf() builds a letter-format jsPDF document with: top meta band ("ROUTE LABEL" left, today's date right, gray, 9pt) over a hairline divider; bold 17pt title with line-wrap; per-section bold 12pt headings ("What is this tariff?", "Legal basis & authority", "Current rate", "Current status" if present, "Last updated", "Political context & background" if present, "TariffCalc Source Data" if present); 11pt body where any HTML tags are stripped with /<[^>]*>/g and HTML entities (&, <, >, link emoji 🔗) are decoded; rate string mirrors openLegal()'s logic (mpf min/max bracket, specific_rate_per_unit override, flat basis, duty_paid basis, plain ad valorem); sources block includes label + clickable doc.textWithLink for each source URL; per-page footer with hairline divider, "GB-BizCalc — Each Tariff Explained · B4.0" left and "Page X of Y" right. Filename: tariff-detail-<safe-name>[-route]-B4.0.pdf so downloads sort cleanly by version stamp alongside every other PDF this app produces. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, the cache-first calculate from B3.6 #1, news fetcher (loadNews + the B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25 cap), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), Customer Card right-click menu (B1.1 #5), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card, history logs, the B3.9 #1-#5 TariffNews FAVORITES + per-article PDF feature set are all byte-for-byte identical to B3.9 except for the timeline-strip empty-state path. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged. Per-click cost unchanged. Version bump from B3.9 -> B4.0 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers (including the new tariff-detail PDF), save-as filenames (every -B3.9.pdf -> -B4.0.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B4.0'. The B3.9, B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B3.9Apr 2026TariffNews FAVORITES + per-article PDF export. Four UX upgrades on top of B3.8. (1) HISTORY-TIMELINE READOUT SIMPLIFIED. The slider readout from B3.8 #1 ("MMM DD HH:MM · N articles") is reduced to the date alone ("MMM DD, YYYY"). The article-count tail is removed because the bright-green dot (B3.8 #1) on each card already marks new arrivals card-by-card, making the per-batch count redundant; dropping the tail also removes the only readout fragment that could overflow once 8-10+ batches were cached. The readout is shifted to the LEFT side of the timeline strip (was right) using flex order:0 so the new FAVORITES button can occupy the right side cleanly. The clock part is gone too because the timeline is meant to be a "which day" affordance, not a "which minute" one. fmt() inside renderNewsTimeline now uses toLocaleDateString({month:short,day:numeric,year:numeric}) instead of the previous toLocaleString with hour/minute opts. labelFor() collapses to fmt(ts) directly. The slider event handlers (onNewsTimelineSliderInput) call the same labelFor so on-drag updates also stay date-only. (2) FAVORITES TOGGLE BUTTON. New .news-favorites-btn rendered to the RIGHT of the slider/readout in the same flex strip. Default state is light yellow (#fff7c0 background, black text) with a subtle 1px hairline border. On hover or focus-visible, the button switches to the bright TariffNews-yellow gradient (linear-gradient 135deg #fdd835 -> #ffeb3b — the exact same gradient used by the .news-peek-tab) with a tiny shadow lift. When clicked it stays bright yellow because the .active class is added to reflect that newsViewMode === 'favorites'. Click again toggles back to light yellow. Sized to match the .live-badge inside the popup (font-size 10px, padding 2px 9px) so it visually pairs with that badge rather than the larger UPDATE NEWS button — exactly the spec ask. Width is auto so it stretches to fit "FAVORITES" comfortably. New module-level state: const NEWS_FAVORITES_KEY = 'tc_news_favorites_v1'; let newsFavorites = new Set(); let newsViewMode = 'all'; let currentNewsModalIdx = -1; loadNewsFavorites() / saveNewsFavorites() persist the Set to localStorage as a JSON array on every star/unstar. (3) PER-ARTICLE STAR. Every news card now renders a faded gray star button (.news-card-star) absolutely positioned at top:8px;left:50% (translateX(-50%)) — top-center of the card, on the same vertical band as the news-tag pill (which floats top-left) and the green new-dot (which floats top-right). The star is an inline SVG path so CSS can swap its fill via .starred. Clicking the star: event.stopPropagation() (so it does not bubble up to the card's openNewsModal click), then toggleFavorite(origIdx) flips the article in/out of newsFavorites and persists. Default star color is #bdbdbd (gray) with a thin rgba(0,0,0,.18) stroke. .starred turns the fill into the same TariffNews yellow #fdd835 with a darker #b58e00 stroke and a 3px drop-shadow glow. Hover scales the star 1.12x and brightens it. The star's aria-pressed reflects state for screen readers. Stable favorite ID = (item.source_url ? "u:"+source_url : "h:"+headline) — favorites survive cache rollover (B3.8 #2) as long as the article body or source URL stays the same, which the merge-upstream code already guarantees. (4) FAVORITES PANEL — same news grid, filtered. When newsViewMode flips to 'favorites', renderNews builds viewItems by walking newsItems and keeping only the entries whose _newsFavId is in newsFavorites. The grid renders newest-first because newsItems is already in that order (B3.8 #2 prepends new batches). Each card keeps its real index via data-orig-idx so toggling the star in favorites mode correctly removes the article from the panel (the next renderNews re-render naturally drops it). The history-timeline slider in favorites mode operates on the FILTERED viewItems — so if you have favorites spread across many refresh batches, the slider only exposes the batches that contain at least one favorite, exactly matching the "becomes a new instance of itself operating solely as the FAVORITES history timeline bar" spec. The greenest-batch detection is recomputed against viewItems so if your most-recent favorite is in the most recent batch the readout still glows green. Empty state: if newsFavorites is empty (or no favorites are present in the cached items), the grid shows "No favorites yet — click the star at the top of any article to add it here." instead of an empty grid. Clicking the FAVORITES button again returns to all-articles view. (5) PER-ARTICLE PDF BUTTON. The news article modal (#newsBg) now hosts a small orange PDF button (.news-modal-pdf-btn, #newsModalPdfBtn) absolutely anchored to right:14px;bottom:14px of the modal box — same orange/white scheme used by every other PDF download in this app (#ff8a1f bg, #fff text, 1px #d96f00 border, slight box-shadow). The modal-body padding-bottom is bumped to 56px so long article text never slides under the button. New exportNewsArticlePdf() function (called by the button onclick): reads currentNewsModalIdx (set inside openNewsModal), fetches newsItems[idx], builds a letter-format jsPDF document with: top meta band (tag pill text + date, gray, 9pt) over a hairline divider; bold 17pt title with line-wrap and auto-pagination; 11pt body where item.full HTML is parsed into a detached <div>, block-level boundaries (p/h1-5/li/br) get \n\n appended so paragraph breaks survive the .innerText flatten; multi-line wrap via doc.splitTextToSize(usableW); page-break check before each line; source link footer with hairline divider, "Source:" label and a clickable doc.textWithLink(URL) of the source_name plus the raw URL in parens; per-page footer with "GB-BizCalc TariffNews · B3.9" left and "Page X of Y" right. Filename: tariff-news-<safe-headline>-B3.9.pdf so downloads sort cleanly by version stamp alongside every other PDF this app produces. Defensive guards: if no article is open (currentNewsModalIdx < 0) we alert; if jsPDF UMD is not yet on window.jspdf.jsPDF we alert and ask the user to retry. closeNewsDirect() now also resets currentNewsModalIdx to -1 so a stale index never survives a close-then-reopen cycle. NO data paths changed: tariff math, fetchLiveRates*() / Update Tariffs path, the cache-first calculate from B3.6 #1, news fetcher (loadNews + the B3.8 #2 prepend-merge with NEWS_MAX_ITEMS=25 cap), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), DUPLICATE PROFILE menu (B3.8 #1), Customer Card right-click menu (B1.1 #5), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card, history logs, all PDF download paths besides the new per-article one are byte-for-byte identical to B3.8. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged. Per-click cost unchanged. Version bump from B3.8 -> B3.9 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B3.8.pdf -> -B3.9.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.9'. The B3.8, B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B3.8Apr 2026DUPLICATE PROFILE on Macro Cost Basis right-click menu, mirroring the Customer Card pattern from B1.1 #5. User asked for the same right-click ergonomics on MACRO COST BASIS - PROFILES that Customer Cards already have: when you right-click a profile slot, the option directly under the cursor should be DUPLICATE PROFILE (one click duplicates immediately, no confirm), and DELETE PROFILE should sit ABOVE the cursor so you have to deliberately move the mouse up to reach it (still requires the two-step → CONFIRM DELETE flow that all destructive actions in this app use). (1) Right-click context-menu attachment widened. In B3.7 the contextmenu DOM attribute on .roi-basis-opt was gated by isBuiltin — built-in basis options (pallet / TEU / FEU) had NO contextmenu wired up, so right-click on those did nothing. The gate is removed in B3.8: every basis option (built-in + custom) now gets oncontextmenu="…roiShowBasisCtxMenu(event,id)". This makes built-ins right-clickable for the first time, but they cannot be deleted (DELETE would orphan the per-basis data on every Customer Card sub-profile and break the macro fallback chain). The menu rendering distinguishes the two cases — see (2). (2) Two-button menu for customs, one-button menu for built-ins. roiShowBasisCtxMenu(evt,id) now branches on ROI_BASES.includes(id) plus the existing roiBasisIsBuiltin check. For CUSTOM profiles (cb1, cb2, …) the menu emits, top-to-bottom: followed by . The .ctx-delete CSS class (existing) gives the top button red text and the same hover-then-confirm pulse the Customer Card DELETE button uses. After cursor positioning the bottom button (DUPLICATE PROFILE) ends up directly under the right-click point (we centre the menu on the cursor with nx = clientX - r.width/2; ny = clientY - r.height/2 — same formula as roiShowCustomerCtxMenu), so a stationary click fires DUPLICATE while reaching DELETE requires moving the cursor up by ~one button height. Exactly mirrors the Customer Card layout. For BUILT-IN profiles only one button is rendered: . There is no DELETE row for built-ins — the menu is shorter and DUPLICATE is still the action under the cursor. The .delete-pending hover-confirm class is only applied to non-built-ins since built-ins have no delete button to gate. (3) roiBasisCtxAction(action) handles the new 'duplicate' branch. 'delete' still routes to the existing two-step → CONFIRM DELETE flow (first click → button repaints to "CONFIRM DELETE?" with a red pulse, second click within 3s → roiBasisDelete(id) actually removes the profile). 'duplicate' is one-shot: it closes the menu and calls roiDuplicateBasis(id) immediately. There is no CONFIRM DUPLICATE step because duplication is non-destructive (worst case the user gets an unwanted slot they can immediately right-click → DELETE). (4) New roiDuplicateBasis(srcId) function modelled on roiDuplicateCustomerCard from B1.1 #5. Steps: allocate a fresh custom basis ID via roiBasisNextCustomId() (returns cb using the same monotonic counter as the + button); compute auto-name by reading the source profile's display name, stripping any trailing " " so we don't end up with "Custom 2 1", and walking n=1,2,3… until "" is unique against the lowercased set of existing display names (cap final length at 28 chars); splice newId into ROI_BASES at srcIdx+1 so the duplicate appears immediately to the right of the source slot in the basis-options strip; register meta entry roiBasisMeta[newId] = { name, locked:false, custom:true } and persist via roiBasisMetaSave(); call roiEnsureBasisBuckets(newId) to seed the global per-basis stores. Deep-copy the source's data via JSON.parse(JSON.stringify(…)) into newId across every per-basis bucket the app uses: roiState (input field values), roiPromos (promo entries), roiBasisLanded (landed-cost overrides), roiBasisLog (history log entries) — all four global stores are persisted. AND iterate every Customer Card record in roiCardData, and within each record iterate every sub-profile, deep-copying sp.state[srcId] -> sp.state[newId], sp.promos[srcId] -> sp.promos[newId], sp.notes[srcId] -> sp.notes[newId] so the duplicate has identical data on every Customer Card × Profile crossing. roiCardDataSave() persists. roiBasisOrderSave() persists the new ROI_BASES order. (5) UX polish: after the duplicate is created, roiRenderBasisOptions() rebuilds the strip and a requestAnimationFrame callback finds the new .roi-basis-opt[data-basis="newId"] element and calls scrollIntoView({behavior:'smooth',block:'nearest',inline:'center'}) so the new slot scrolls into view if it landed off-screen. Same affordance the + button uses when adding a fresh empty profile. (6) NO data paths changed otherwise: tariff math, fetchLiveRates*() / Update Tariffs path, the cache-first calculate from B3.6 #1, news fetcher + UPDATE NEWS button + slider history bar (B3.7 #1), drag-to-reorder for Customer Cards and Macro Cost Basis (B3.7 #2), Customer Card right-click menu (B1.1 #5), padlock/rename/+/has-data dot rendering, sub-profile drag-drop within a card, history logs, PDF download paths are byte-for-byte identical to B3.7. The B1.1/B1.2 swap-confirm popup logic (kept as dead-but-not-removed since B3.7) remains untouched. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged. Per-click cost unchanged. Version bump from B3.7 -> B3.8 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B3.7.pdf -> -B3.8.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.8'. The B3.7, B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B3.7Apr 2026Two news/UX upgrades on top of B3.6. (1) NEWS HISTORY SLIDER replaces the dot-tick row from B3.5/B3.6. The previous timeline rendered one dot per refresh batch with a date+count label below each dot, which worked fine for two or three batches but was destined to overlap badly once the cache filled with 8-10+ batches. The new design is a single horizontal slider (native input type=range): left = newest batch, right = oldest. Click anywhere on the track or drag the green thumb and it snaps to the nearest discrete batch position (range step=1, mapped to batch index). One mono-font date+article-count readout sits to the right of the track and updates LIVE as the slider moves (oninput). On release / track-click (onchange) the news grid scroll-snaps to the first card of the selected batch and the destination card briefly flashes with the accent ring (same flash effect as the old tick-click). Readout text turns green and bold when the slider is parked on the most-recent batch. Slider is keyboard-accessible (left/right arrows step through batches) and has an aria-label so screen readers describe it as the news history slider. Implementation: renderNewsTimeline() now emits .news-timeline-slider + .news-timeline-readout instead of the old .news-timeline-track + .news-timeline-tick row; new onNewsTimelineSliderInput() handles the live readout update; new onNewsTimelineSliderChange() handles the snap-scroll on release; scrollToNewsBatch() simplified to drop the now-unused tickEl highlight. Batch lookup tables (order array, ts->info Map, fmt fn) are stashed on the timeline element so the handlers can read them without depending on a stale closure across re-renders. CSS for the old tick / track / spacer / tick-label rules is removed; new .news-timeline-slider rules style the WebKit + Firefox track/thumb (16px green thumb #1cd24a with the same green-glow as the B3.5 newest-tick), readout uses tabular-nums for stable width as the date string changes. (2) DRAG-TO-REORDER restored on Customer Cards AND Macro Cost Basis options. In B1.1 #4 the Macro Cost Basis drag-drop was repurposed from "reorder buttons" to "data-swap with confirm popup", and B1.2 #1 did the same to Customer Cards. The user asked for the original reorder behavior back so they can reposition slots on the fly. Both drop handlers (roiCardDrop + roiBasisDrop) now splice the source slot before/after the target based on cursor X relative to the target slot center: cursor on left half = insert before, right half = insert after. The dragOver handlers paint the existing left/right hairline accent indicator on the hovered slot (the .drag-over-left / .drag-over-right CSS rules from B1.1 still exist and are now used again). dragstart effect changed from "copy" to "move" so the cursor affordance matches intent. After a successful drop, roiCustomerCards (or ROI_BASES) is spliced in place, persisted via roiCardsSave (or roiBasisOrderSave), and the strip is re-rendered. The data-swap-confirm popup logic from B1.1/B1.2 (roiCardSwapConfirmClick, _roiShowCardSwapConfirm, roiHideCardSwapConfirm + the roiBasisSwap* twins) remains in the file as dead-code-but-not-removed — none of it is wired up to any drag-drop path now, so dragging never opens the swap-confirm popup. The popup HTML elements (#roiCardSwapConfirm, #roiBasisSwapConfirm) are kept in the DOM in case a future feature wants to re-expose data-swap via a different trigger (e.g. right-click menu). All other interactions on these slots — left-click to select, right-click to open the per-slot context menu, padlock toggle, name rename, +-button to add a slot, has-data dot rendering, sub-profile drag-drop within a card — are unchanged from B3.6. No data paths touched: tariff calculation, cache-first calculate (B3.6 #1), news fetcher, Update Tariffs / Canada / Mexico fetchers, history logs, news popup features (UPDATE NEWS button, green-dot per batch, source link), and PDF download paths are byte-for-byte identical to B3.6. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged. Per-click cost unchanged. Version bump from B3.6 -> B3.7 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B3.6.pdf -> -B3.7.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.7'. The B3.6, B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows are intentionally preserved as historical record.
B3.6Apr 2026Cache-first Calculate Tariffs - speed fix on top of B3.5. User reported that after clicking Calculate Tariffs / Recalculate Tariffs there was a long processing pause (5-30 seconds) before the tariff totals updated, and the slowdown grew worse as more import / export country combos were used. Root cause: the calculate() function in B3.5 (and every prior release back through B1.6) always called fetchLiveRates / fetchLiveRatesCanada / fetchLiveRatesMexico whenever navigator.onLine was true, which means every Calculate click hit the Anthropic API plus 1-2 web_search tool calls regardless of whether identical rates were already sitting in localStorage from a recent Update Tariffs run. The localStorage cache (key tc_tariffs_v5, namespaced by import:origin:hts) was effectively only being read on the offline branch. Fix: calculate() is now cache-first across all three import-country branches (US, CA, MX). Each branch first calls loadCache(CACHE_KEY, key) for its appropriate namespaced key (cacheKey(hts) for CA/MX, hts || product for US). On cache HIT (the common case after the user has clicked Update Tariffs at least once for that combo) the cached AdaValorem / Section301 / IEEPA / Reciprocal / Section232 / MFN payload is used immediately and the calculation completes in essentially zero time - the same instant feedback the user remembers from earlier sessions. On cache MISS (first calculation for a brand-new import-origin-HTS combination) we fall through to the live fetch path: showFetchingLabel() flips the spinner label, the fetchLiveRates*() function is called, the result is saved to cache, and a "Live tariff rates verified" banner appears as before. If the live fetch throws (network blip, rate-limit, API error) we fall through to buildFallback*() exactly as the offline path would. Spinner UX: the default spinner label is now "Calculating…" rather than "Fetching current rates…" - the latter only appears when we actually hit the network, so on the cache-hit fast path the user sees a brief "Calculating…" flash and then the result. The Update Tariffs button retains its existing semantics as the explicit "refresh rates now" trigger - it always bypasses the cache, hits the live AI-assisted web-search path, and overwrites the cached entry, so the user has a clear way to force-refresh stale rates. User Guide §6 prose updated to document the new cache-first behavior, the spinner label transition, and the explicit Calculate-vs-Update split. NO data paths changed otherwise: tariff math, fetchLiveRates*() prompts, web_search wiring, history logs, news popup (UPDATE NEWS button + green-dot + history timeline + source link from B3.5), and PDF download paths are byte-for-byte identical to B3.5. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged - the cache-first path actually REDUCES API spend because repeat Calculate clicks on the same combo no longer cost anything. Per-click cost when the cache is warm: $0.00. Per-click cost on cache miss: identical to B3.5. Version bump from B3.5 -> B3.6 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B3.5.pdf -> -B3.6.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.6'. The B3.5, B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows and the inline 'B3.5 #N' / 'B3.4 #N' / 'B3.3 #N' / 'B3.2 #N' / 'B3.1 #N' / 'B3.0 #N' / 'B2.9 #N' code-comment markers are intentionally preserved as historical record.
B3.5Apr 2026TariffNews UX trio on top of B3.4. The merge-instead-of-replace fix shipped in B3.4 was correct (prior articles now stay scrollable below the new ones, 25-cap with FIFO eviction, source link at bottom of each article modal) but the user asked for three additional usability features layered on top. (1) Green "new" dot - every news card from the most recent UPDATE NEWS click now wears a small bright-green dot (#1cd24a, 9px, top-right corner with a 2px surface ring + soft outer glow) so the user can spot at a glance which articles came in on the latest fetch versus which were already cached. The dot persists until the next UPDATE NEWS click, at which point newestBatchTs jumps and the dot migrates to the just-arrived batch. Implementation: mergeNewsItems(newItems, priorItems, cap, batchTs) now stamps every fresh item with __fetchedAt = batchTs (one shared Date.now() per refresh batch). Prior items keep their existing __fetchedAt; pre-B3.5 cached items get __fetchedAt = 0 backfilled. renderNews() scans for max(__fetchedAt) and adds .news-card-newdot to every card whose __fetchedAt equals that max (so all 3 fresh items in the latest batch get a dot, none of the older items do). (2) UPDATE NEWS button - the small flat-link "↻ Refresh" was upgraded to a proper accent-colored button labeled "↻ Update News" (uppercase, 6px-radius, white text on accent background, hover lifts -1px with a stronger shadow, active settles back). New CSS class .news-update-btn replaces .news-refresh on this one button (the .news-refresh class still exists in CSS for backward-compat with the loading state styling). The setNewsRefreshFeedback() loading-class toggle continues to work because we kept the .loading rule and updated .news-update-btn.loading to mirror the same opacity/cursor:wait behavior. (3) Horizontal history timeline - a new .news-timeline strip sits directly above the news grid (between the header bar and the article cards). Hidden by default and only shown when there is more than one batch of articles in cache. Layout: a "History →" label on the left, then a horizontal track with one .news-timeline-tick per batch (newest on the LEFT in green, older batches further to the right with neutral border styling). Spacers between ticks distribute them across the track width. Hovering a tick scales it up and tints the date label below it; clicking a tick (a) scrolls #newsArea down to the first card of that batch using card.offsetTop arithmetic (NOT scrollIntoView, which would scroll the page), (b) highlights the tick with .active accent styling, and (c) flashes the destination card with a 0.9s box-shadow ring so the user can see where they landed. Tick title attribute shows e.g. "Jump to articles from Apr 27 4:30 PM (3 articles)". Below each tick a small mono-font date label like "Apr 27 4:30PM · 3" doubles as a permanent timestamp. New helper renderNewsTimeline(items, newestBatchTs) builds the markup; called at the end of renderNews(). New helper scrollToNewsBatch(firstIndex, tickEl) handles tick clicks. Timeline auto-hides when there is only a single zero-timestamp batch (e.g. fresh cache from pre-B3.5 with no real timestamps yet) so it doesn't look broken. User Guide §12 prose updated to document Update News button, Green dot, History timeline, and Source link as four bullets. No data paths touched. Tariff calculation paths, Update Tariffs / Canada / Mexico fetchers, history logs, and PDF downloads byte-for-byte unchanged from B3.4 - this release is news-popup-only. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged. Per-click cost essentially unchanged (the schema fields and dedup keys are unchanged from B3.4). Version bump from B3.4 -> B3.5 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B3.4.pdf -> -B3.5.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.5'. The B3.4, B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows and the inline 'B3.4 #N' / 'B3.3 #N' / 'B3.2 #N' / 'B3.1 #N' / 'B3.0 #N' / 'B2.9 #N' code-comment markers are intentionally preserved as historical record.
B3.4Apr 2026TariffNews UX upgrades on top of B3.3. Two related complaints: (1) after a successful Refresh, two of the five previously-cached articles disappeared - the user expected new articles to be PREPENDED above the prior ones with the older ones still scrollable below, not to wholesale replace the cache; (2) when reading an article in the deep-modal there was no way to jump to the underlying primary source (government release, federal register notice, court filing, or news outlet) to verify the claim. Three-part fix: (1) MERGE INSTEAD OF REPLACE - new helper getNewsCacheRaw() returns cached items regardless of TTL (TTL gating still applies to the auto-display path via getNewsCache(), but the merge path needs the raw historical list). New helper mergeNewsItems(newItems, priorItems, cap) does case-insensitive trimmed-headline dedup via a Set, prepends fresh items above prior items, and slices to cap. The loadNews() success path now does priorRaw=getNewsCacheRaw(); merged=mergeNewsItems(items,priorRaw,NEWS_MAX_ITEMS); setNewsCache(merged); renderNews(merged,true) instead of the old setNewsCache(items); renderNews(items,true). The new-headline counter still compares fresh.headline against priorHeadlines (so the "✓ N new (M total)" toast counts only genuinely new items, where M is now merged.length not items.length). (2) FIFO 25-CAP - new constant NEWS_MAX_ITEMS=25 enforces a hard ceiling. Because mergeNewsItems prepends new items first then dedupes, when the cap is exceeded the OLDEST trailing items get sliced off (FIFO eviction). NEWS_CACHE_TTL was also extended from 4 hours to 90 days so the merge has plausible historical context to merge against - the auto-popup behavior on app load is unchanged because B3.2 already removed the auto-fire of openNewsPopup; the TTL only governs whether getNewsCache() (gated path) returns cached items for silent display, and 90 days is well-aligned with the user expectation that prior articles stick around until naturally pushed out by the 25-cap. (3) SOURCE LINK IN MODAL - the news article schema gained two new fields: source_url (REQUIRED https URL pointing at the primary source - gov release, federal register, court filing, or major-outlet article) and source_name (OPTIONAL short label like "WSJ" / "Reuters" / "CBP" / "White House"; if omitted, the URL host is shown with the leading "www." stripped). The prompt explicitly instructs Sonnet to use the actual URL it found via web_search rather than guess, and adds "The source_url field is mandatory - do not return any item without one." Old cached items without source_url simply don't render the link block (no breakage). The openNewsModal() renderer appends a div.news-modal-source to bodyHtml AFTER the existing item.full HTML, with the link wrapped in target="_blank" rel="noopener noreferrer" for safe external navigation. URL and label are HTML-escaped (&, ", <, >) before injection. New CSS rule .news-modal-source styles a small top-bordered footer block below the article body with the accent color on the link. word-break:break-all on the anchor prevents long URLs from overflowing the modal width on narrow screens. No data paths touched: tariff calculation, Update Tariffs / Canada / Mexico fetchers, history logs, and PDF downloads are byte-for-byte unchanged - this release is news-only. Pricing constants ($3 per 1M input, $15 per 1M output, $0.05 per web search) and max_tokens=5000 unchanged from B3.3. Per-click cost essentially unchanged (the two new schema fields add maybe 30-60 output tokens per article = ~$0.001 total). Version bump from B3.3 -> B3.4 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B3.3.pdf -> -B3.4.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.4'. The B3.3, B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows and the inline 'B3.3 #N' / 'B3.2 #N' / 'B3.1 #N' / 'B3.0 #N' / 'B2.9 #N' code-comment markers are intentionally preserved as historical record.
B3.3Apr 2026News-fetcher robustness hotfix on top of B3.2. After B3.2 decoupled openNewsPopup() from the news API call, the explicit Refresh button still failed with "Refresh failed: No JSON array" on subsequent clicks. Diagnosis: the failure was NOT the same root cause as the earlier "Expected ]" parse error - it was a different symptom of the same underlying problem. The strict regex requires both an opening AND a closing bracket; when Sonnet hits max_tokens (3000 in B3.1/B3.2) before finishing the array, the regex returns null and the code throws "No JSON array" - which is technically correct (there is no complete array) but unhelpful. The truncation kept happening because (a) Sonnet writes narrative text between web_search tool calls ("Let me search for..." / "Based on these results...") which consumes output tokens, (b) the per-article 'full' HTML was asked for as "4-6 paragraphs" plus optional timeline divs which is ~600-1000 tokens per article, and (c) at 3 articles that's ~2-3K tokens of HTML alone, with no headroom for the inter-search narrative. Four-part fix: (1) max_tokens 3000 -> 5000 - now that openNewsPopup no longer auto-fires (B3.2 removed it), news refresh is fully decoupled from Update Tariffs, so it can spend more of the per-minute OTPM budget on a single explicit click. Pairing it with Update Tariffs in the same minute would require ~5000+4000=9000 OTPM which exceeds tier-1's ~8000/min cap, but in that case the existing 429 hint already tells the user to wait 60s. (2) Tightened the prompt: 'full' HTML asks for "2-3 short paragraphs" instead of "4-6 paragraphs", drops the timeline div requirement, and adds an explicit "keep each 'full' under 600 characters" hint. Same article count (3) so the JSON array shape is unchanged. Also added "Do NOT include any narrative text before or after the array" to discourage the chatty narrative that consumed output tokens between searches. (3) New stop_reason inspection - if d.stop_reason is 'max_tokens' the error message now reads "No JSON array - Sonnet was cut off mid-response by max_tokens. Try refresh again, or wait 60s if rate limit also hit" instead of a bare "No JSON array". Also handles 'tool_use' as a separate case ("stopped on a tool_use turn before generating the JSON"). (4) JSON-recovery fallback - when the strict regex fails OR JSON.parse throws on the matched substring, a new bracket-walking parser scans the raw text from the first opening-bracket, tracks string/escape state and brace depth, and remembers the offset right after each complete top-level closing-brace. If the response was truncated mid-object, we slice up through the last complete object and wrap with closing-bracket to recover at least 1-2 articles instead of zero. Only throws "No JSON array" if literally zero usable objects can be salvaged. Pricing constants ($3 per 1M input tokens, $15 per 1M output tokens, $0.05 per web search) unchanged. Per-click cost goes up modestly because the bigger max_tokens reservation reserves more OTPM (but only the tokens actually consumed are billed), so a typical successful refresh now lands around $0.06-$0.10 instead of $0.04-$0.06. No data paths touched. Tariff calculation paths byte-for-byte unchanged. The Update Tariffs / Canada / Mexico fetchers are byte-for-byte unchanged - this fix is news-only. Version bump from B3.2 -> B3.3 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every -B3.2.pdf -> -B3.3.pdf), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.3'. The B3.2, B3.1, B3.0, B2.9, B2.8 changelog rows and the inline 'B3.2 #N' / 'B3.1 #N' / 'B3.0 #N' / 'B2.9 #N' code-comment markers are intentionally preserved as historical record.
B3.2Apr 2026User-experience hotfix on top of B3.1. After B3.1's rate-limit tightening got Update Tariffs working ($0.31 / click on Sonnet 4.5 with 2 web searches), the user noticed that the TariffNews panel always seemed to fire its own API call right after Update Tariffs — and that second call kept hitting 429 even with B3.1's smaller max_tokens budget. Diagnosis: it was NOT a coupling between the two buttons. The real culprit was openNewsPopup() (added in version A9.4), which auto-fired loadNews(true, { userInitiated: true }) every single time the user opened the news panel popup. So the sequence was: click Update Tariffs (consumes ~50% of tier-1 OTPM) → open the news panel to see what was new → openNewsPopup() silently fires a second forced fetch → 429 because the OTPM budget hadn't reset yet. Three problems with the original behavior: (a) every popup glance burned $0.10–$0.30, (b) it tripped tier-1 rate limits when paired with any other API-using click in the same minute, and (c) it made the explicit ↻ Refresh button look redundant since the panel already refreshed itself on open. Fix: removed the auto-fetch from openNewsPopup() entirely. The popup now ALWAYS serves from cache, and the cache is populated once by the page-load loadNews(false) call. The ↻ Refresh button is now the ONLY way to fetch fresh news articles. This makes Update Tariffs and TariffNews refresh truly independent — you can click one without it costing you a budget allocation toward the other. Also fixed the 'No JSON array' truncation error on news refresh: with B3.1's news max_tokens at 3000, asking for 5 articles with rich 'full' HTML payloads was over budget — Sonnet was cutting off mid-array and the regex couldn't find a closing ']'. Reduced article count 5 → 3 in the prompt so the closing bracket always fits inside 3000 tokens. The 10-article safety cap in the post-fetch JS stays unchanged as a defensive ceiling. No data paths touched. Tariff calculation paths byte-for-byte unchanged. Pricing constants unchanged. Version bump from B3.1 → B3.2 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every '-B3.1.pdf' → '-B3.2.pdf'), Save-a-copy HTML download. Internal VERSION constants updated to 'B3.2'. The B3.1, B3.0, B2.9, B2.8 changelog rows and the inline 'B3.1 #N' / 'B3.0 #N' / 'B2.9 #N' code-comment markers are intentionally preserved.
B3.1Apr 2026Tier-1 rate-limit hotfix on top of B3.0. After B3.0 shipped the 429 / JSON-parse / 404 fixes, the user's very first click of Update Tariffs still returned 429 — and clicking TariffNews ↻ Refresh after that also returned 429 in the same minute. Diagnosis: the 429 was coming from the Anthropic OUTPUT-tokens-per-minute (OTPM) limit, not the input limit. Tier-1 Sonnet OTPM is roughly 8K/min, and Anthropic pre-allocates max_tokens against that budget at request time. B3.0 set max_tokens to 8000 for Update Tariffs which equals 100% of tier-1 OTPM — every click burned the whole minute's budget on a single request, so any second call inside 60 seconds (including the news refresh) instantly tripped the limiter. Fixes: (1) Update Tariffs / CA / MX max_tokens dropped 8000 → 4000 (~50% of tier-1 OTPM, room for two calls per minute). News max_tokens dropped 4500 → 3000 (~38% OTPM). (2) Added max_uses: 2 to the web_search tool in all three tariff fetchers and max_uses: 3 in the news fetcher — the unbounded default lets Sonnet rip through many searches whose results balloon input-token consumption (each search response is fed back as input on the next turn), so capping it keeps a single click well under the tier-1 ITPM ceiling (~30K/min). Sonnet's own April 2026 knowledge plus 2-3 verification searches is plenty for the rate context the prompt asks about. (3) Bumped error-text exposure from 120 → 240 chars at every API failure site so the user can see the full Anthropic rate_limit_error body — including which specific limit was hit (input vs output vs requests) and the retry-after hint. The B3.0 status→hint translator (429 / 401 / 403 / 404 / 529) is unchanged. If 429s persist on tier-1 even after these tightenings, the user has two paths: (a) wait 60 seconds between any two API-using clicks, or (b) load $100+ of credits at console.anthropic.com / Plans & Billing to auto-promote to tier 2, where Sonnet limits roughly triple. Pricing constants ($3 per 1M input tokens, $15 per 1M output tokens, $0.05 per web search) remain accurate. No data paths touched. Tariff calculation paths byte-for-byte unchanged. Version bump from B3.0 → B3.1 throughout: title bar, User Guide stamps, version-badge, all PDF headers/footers, save-as filenames (every '-B3.0.pdf' → '-B3.1.pdf'), Save-a-copy HTML download "GB-BizCalc - B3.1 (with-data).html". Internal VERSION constants (TariffCalc + MFR-CostCalc) updated to 'B3.1'. The B3.0, B2.9, B2.8 changelog row narratives and the inline 'B3.0 #N' / 'B2.9 #N' code-comment markers are intentionally preserved as-is — they document when each feature originally shipped.
B3.0Apr 2026Followup hotfixes to the B2.9 release after first real-world test. Three issues surfaced when the user clicked Update Tariffs and TariffNews ↻ Refresh: (1) Anthropic returned 404 on every call because the model alias 'claude-sonnet-4-20250514' was deprecated in early 2026 — replaced with the current stable alias 'claude-sonnet-4-5' in all four fetch sites. (2) Update Tariffs threw 'JSON Parse error: Expected ]' because max_tokens was set so low (3000 for USA, 3500 for Canada/Mexico) that Sonnet's response was truncated mid-array — the regex grabbed an unclosed bracket and JSON.parse choked. Bumped max_tokens to 8000 in all three tariff fetchers so 6 active + 3 inactive tariffs with full description fields fit comfortably. (3) TariffNews ↻ Refresh hit a 429 rate-limit on the very first click because Anthropic's tier-1 default (~30K input tok/min) is easy to blow past with 10 articles × 6000 max_tokens × web_search subqueries. Reduced the news prompt to 5 articles and dropped news max_tokens 6000 → 4500, which keeps a single click well under the tier-1 ceiling. Also added a status→hint translator at every API failure site: 429 → 'rate limit hit, wait 60s and try again', 401 → 'bad or expired API key, re-paste in 🔑', 403 → 'key lacks access to this model', 404 → 'model name not found', 529 → 'Anthropic overloaded, try again in a minute' — so the actionable next step is on screen instead of a bare HTTP code. The news error path now also captures the response body the same way the tariff fetchers do, so 4xx/5xx errors surface their full Anthropic message instead of just the status number. Pricing constants ($3 per 1M input tokens, $15 per 1M output tokens, $0.05 per web search) remain accurate — Anthropic kept Sonnet pricing flat across the 4 → 4.5 → 4.6 lineage, so the Cost Per Click readout stays correct. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Tariff calculation paths for U.S. / Canada / Mexico imports are byte-for-byte unchanged. Version bump from B2.9 → B3.0 throughout: title bar (GB-BizCalc - B3.0), User Guide pill stamp + modal header stamp, version-badge ("Version B3.0"), all PDF headers/footers, save-as filenames (tariff-analysis-*-B3.0.pdf, mfr-cost-analysis-*-B3.0.pdf, roi-*-B3.0.pdf, ship-calc-*-B3.0.pdf, trade-terms-chart-B3.0.pdf, gb-bizcalc-user-guide-*-B3.0.pdf, gb-bizcalc-changelog-*-B3.0.pdf, gb-bizcalc-user-guide-tariffs-*-B3.0.pdf, gb-bizcalc-user-guide-all-tariffs-B3.0.pdf, gb-tariff-history-B3.0.pdf), Save-a-copy HTML download "GB-BizCalc - B3.0 (with-data).html". Internal VERSION constants (TariffCalc + MFR-CostCalc) updated to 'B3.0'. The B2.9 and B2.8 changelog row narratives and the inline 'B2.9 #N' code-comment markers are intentionally preserved as-is — they document when each feature originally shipped.
B2.9Apr 2026API call regression fix. All four Anthropic API call sites — fetchLiveRates() [USA imports], fetchLiveRatesCA() [Canada imports], fetchLiveRatesMX() [Mexico imports], and the TariffNews refresh in TariffNews-tab Refresh button — were sending an invalid request header ('anthropic-dangerous-request-header': 'true') instead of the actual Anthropic browser-access opt-in header ('anthropic-dangerous-direct-browser-access': 'true'). Result: every browser-origin call to api.anthropic.com was blocked at the CORS preflight stage before the request ever reached Anthropic, so Update Tariffs and the TariffNews ↻ Refresh button both silently failed with no cost-log entry and only a 5-second red status flash that was easy to miss. The fix replaces the wrong header in all four spots so browser-origin calls actually pass Anthropic's CORS check. Error visibility also improved: the catch handler in updateTariffs() now displays '✗ Update failed: <real error message>' and no longer auto-clears after 5 seconds, so a real CORS / 401 / 400 stays on screen until the next click. Same change applied to setNewsRefreshFeedback()'s 'error' state — the actual error text is now surfaced and the 6-second auto-clear is removed for real failures only (success/cached/offline still auto-clear as before). Hover-title shows the full untruncated error message on desktop. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Tariff calculation paths for U.S. / Canada / Mexico imports are byte-for-byte unchanged. Version bump from B2.8 → B2.9 throughout: title bar (GB-BizCalc - B2.9), User Guide pill stamp + modal header stamp, version-badge ("Version B2.9"), all PDF headers/footers, save-as filenames (tariff-analysis-*-B2.9.pdf, mfr-cost-analysis-*-B2.9.pdf, roi-*-B2.9.pdf, ship-calc-*-B2.9.pdf, trade-terms-chart-B2.9.pdf, gb-bizcalc-user-guide-*-B2.9.pdf, gb-bizcalc-changelog-*-B2.9.pdf, gb-bizcalc-user-guide-tariffs-*-B2.9.pdf, gb-bizcalc-user-guide-all-tariffs-B2.9.pdf, gb-tariff-history-B2.9.pdf), Save-a-copy HTML download "GB-BizCalc - B2.9 (with-data).html". Internal VERSION constants (TariffCalc + MFR-CostCalc) updated to 'B2.9'. The historical B2.8 changelog row narrative is intentionally preserved as-is — it documents when the §9 reorganization originally shipped. Also fixed a model-string regression: the four fetch sites were calling 'claude-sonnet-4-20250514' which Anthropic deprecated, returning a 404 not_found_error on every click. Updated to 'claude-sonnet-4-5', the current stable Sonnet alias as of April 2026. Pricing constants ($3 per 1M input tokens, $15 per 1M output tokens, $0.05 per web search) remain accurate — Anthropic kept Sonnet pricing flat across the 4 → 4.5 → 4.6 lineage, so the Cost Per Click readout stays correct without changes.
B2.8Apr 2026User-Guide §9 reorganization on top of B2.7. The "Each Tariff Explained — Routes" section now groups the six route panels by IMPORT country — USA first (9.1 USA Import from CHINA Export, 9.2 USA Import from MEXICO Export), then Canada (9.3 Canada Import from CHINA Export, 9.4 Canada Import from USA Export), then Mexico (9.5 Mexico Import from CHINA Export, 9.6 Mexico Import from USA Export). Previously the order was 9.1 US:CN / 9.2 CA:CN / 9.3 CA:US / 9.4 MX:CN / 9.5 US:MX / 9.6 MX:US which split USA between 9.1 and 9.5 and split Mexico between 9.4 and 9.6 — the new grouping keeps every destination country contiguous so users can find a route without scanning the whole TOC. Edits: (a) The inner Section-9 Table of Contents (#how-tariffs-toc-anchor) was reordered with new 9.x prefixes. (b) The six h3 + body blocks were physically reordered in the HTML so the User Guide flows USA → Canada → Mexico from top to bottom, with the displayed 9.x prefix in each h3 remapped to match. (c) The JavaScript metadata that drives every route-aware feature was rebuilt to the new order: UG_ROUTE_KEYS array, UG_ROUTE_LABELS map, UG_ROUTE_NUMBERS map (US:CN→9.1, US:MX→9.2, CA:CN→9.3, CA:US→9.4, MX:CN→9.5, MX:US→9.6), and UG_ROUTE_ANCHORS map. ugRenderAllRoutes(), ugExportRoutePDF(), ugExportAllTariffsPDF()'s internal TOC + walk order, and ugOpenTariffHistoryRoute()'s modal title all read from these maps directly so they automatically pick up the new order with no further code changes. (d) Anchor IDs (#how-tariffs-us-cn / -us-mx / -ca-cn / -ca-us / -mx-cn / -mx-us) were intentionally left UNCHANGED so any pre-existing deep links keep resolving — only the position in the page and the displayed 9.x prefix moved. (e) Version bump: every B2.7 user-facing reference replaced with B2.8 — title bar (GB-BizCalc - B2.8), User Guide pill stamp + modal header stamp, version-badge ("Version B2.8"), all PDF headers/footers, save-as filenames (tariff-analysis-*-B2.8.pdf, mfr-cost-analysis-*-B2.8.pdf, roi-*-B2.8.pdf, ship-calc-*-B2.8.pdf, trade-terms-chart-B2.8.pdf, gb-bizcalc-user-guide-*-B2.8.pdf, gb-bizcalc-changelog-*-B2.8.pdf, gb-bizcalc-user-guide-tariffs-*-B2.8.pdf, gb-bizcalc-user-guide-all-tariffs-B2.8.pdf, gb-tariff-history-B2.8.pdf), Save-a-copy HTML download "GB-BizCalc - B2.8 (with-data).html". Internal VERSION constants (TariffCalc + MFR-CostCalc) updated to 'B2.8'. Historical "B2.7 #N" / "B2.7 PASS-N" code-comment markers and the B2.7 changelog row narrative are intentionally preserved as-is — they document when each feature originally shipped. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Tariff calculation paths for U.S. / Canada / Mexico imports are byte-for-byte unchanged.
B2.7Apr 2026Three coordinated bug fixes plus a User-Guide affordance, all on top of B2.6. (1) MFR-CostCalc tab finally renders again. The B2.5 PASS-3 changelog row had introduced literal <div data-route="IMP:ORIGIN"> / <h3> / <body> tokens inside a <span class="changelog-note"> narrative AND an unrelated stray zero-indent </div> close inside #panelNews's disclaimer block. The two extras canceled in raw count (414 opens / 414 closes) so a naïve balance audit did not flag the file as broken — but stack-paired they shifted every </div> one slot deeper than intended, prematurely closing <div class="tab-panel-shell"> at the panelNews disclaimer footer instead of after the panelShip block. The result was that #panelMfr, #panelRoi, and #panelShip became nested DESCENDANTS of #panelTariff at parse time, so switchTab('mfr') correctly toggled the panel's own .active class + inline display:'block' but the parent #panelTariff was still display:none on every tab except Tariff-Calc — clipping the entire MFR-CostCalc subtree. Fix: every literal HTML token inside changelog-note narrative spans is now properly entity-escaped (<...>) — including the previously-missed <integer> / <label class="roi-basis-opt"> / <old-value> tokens in older B0.x and A9.x rows — and the stray un-paired </div> was deleted from inside panelNews. Stack-paired audit now shows each panel closing where its <!-- end panel{Name} --> comment lives (panelTariff at 3255, panelMfr at 3435, panelRoi at 3692, panelShip at 3785, tab-panel-shell at 3791, shell at 3794) with zero unmatched closes. Everywhere the rendered DOM has #panelMfr as a direct child of #tabPanelShell, MFR-CostCalc renders again on first click. (2) TariffNews article popup paint regression resolved. Clicking a news article from the ticker peek-tab while sitting on any non-Tariff-Calc tab created the article popup but it was invisible until the user switched back to Tariff-Calc. Root cause was that #newsBg lived inside #tabPanelShell as a sibling of the four panels — and although it carries position:fixed + z-index:920 + inset:0 (which on its own should escape every ancestor's display state), some tab combinations were leaving the modal container inside a hidden subtree on the first open. Fix: openNewsModal() now reparents #newsBg to <body> before adding the .on class, exactly the same pattern openHow / ugOpenTariffHistory / ugOpenChangelogFull already use for their backdrops. Now news article popups paint over the active tab's content regardless of which tab the user is on when they click an article. (3) NEW "↑ Back to Tariff Table of Contents" affordance — every one of the six route panels (9.1 USA Import / 9.2 Canada Import-CN / 9.3 Canada Import-US / 9.4 Mexico Import-CN / 9.5 USA Import-MX / 9.6 Mexico Import-US) under User Guide §9 "Each Tariff Explained — Routes" now ends with a small blue chip-style back button that scrolls the User Guide back up to the inner Section-9 routes TOC anchor (#how-tariffs-toc-anchor). It uses the existing .how-back-top class so it visually matches the existing per-section "↑ Back to Table of Contents" buttons that live at the bottom of every other User-Guide section. Implementation: ugRenderRoutePanel() appends the button HTML at the end of every panel render, so the button persists through every Update Tariffs refresh + every fetchLiveRates*() round-trip on every route. The same back-chip is also injected into the bottom of every individual tariff full-detail modal (the popup that opens when a user clicks one of the clickable tariff <h3> entries inside a route panel) — clicking it closes the modal and scrolls the User Guide back to the routes TOC, so users can quickly bounce between tariffs without manually scrolling. A new closeLegalAndScrollToTariffsTOC() helper wires the click. (4) Version bump: every B2.6 user-facing reference replaced with B2.7 — title bar (GB-BizCalc - B2.7), User Guide pill stamp + modal header stamp, version-badge ("Version B2.7"), all PDF headers/footers, save-as filenames (tariff-analysis-*-B2.7.pdf, mfr-cost-analysis-*-B2.7.pdf, roi-*-B2.7.pdf, ship-calc-*-B2.7.pdf, trade-terms-chart-B2.7.pdf, gb-bizcalc-user-guide-*-B2.7.pdf, gb-bizcalc-changelog-*-B2.7.pdf, gb-bizcalc-user-guide-tariffs-*-B2.7.pdf, gb-bizcalc-user-guide-all-tariffs-B2.7.pdf, gb-tariff-history-B2.7.pdf), Save-a-copy HTML download "GB-BizCalc - B2.7 (with-data).html". Internal VERSION constants (TariffCalc + MFR-CostCalc) updated to 'B2.7'. Historical "B2.6 #N" / B2.5 / B2.1 / B0.x / A9.x changelog row narratives are intentionally preserved as-is — they document when each feature originally shipped. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Tariff calculation paths for U.S. / Canada / Mexico imports are byte-for-byte unchanged.
B2.6Apr 2026Hotfix release — User Guide "Full Guide PDF" was downloading only the Table of Contents and then stopping, leaving the rest of the User Guide off the export. Root cause: B2.5 PASS-3's Section-9 restructure added new top-level children to #howBody (six route <h3 class="ug-route-h3"> headings with embedded GB-Tariff-History + PDF buttons, an inner Section-9 TOC, and six <div data-route="…" class="ug-route-body"> panels populated by ugRenderRoutePanel), and one early element in the per-section walk threw inside _ugPdfEmitNode — most likely on a transient placeholder paragraph or a heading whose textContent included nested button labels with characters jsPDF helvetica couldn't encode. The un-caught exception silently aborted the for…of loop in ugExportFullGuidePDF, leaving the PDF with only the title block + the explicit TOC pass that runs before the loop. Fix: (1) Every per-element emission in ugExportFullGuidePDF / ugExportSectionPDF / ugExportRoutePDF / ugExportAllTariffsPDF and inside the recursive _ugPdfEmitNode is now wrapped in try/catch with a console.warn fallback, so a single bad node can never abort the whole walk. (2) New helper _ugPdfHeadingText() reconstructs heading strings from non-button child nodes — it strips nested <button>, .ug-pdf-btn-sm/-lg/-cl/-blue, .ug-all-tariffs-pdf-btn, .ug-tariff-hist-btn, .ug-cl-big-btn, and .how-back-top elements so headings emit clean text ("9.1 USA Import Tariffs (from CHINA Export)") instead of "9.1 USA Import Tariffs (from CHINA Export)GB - Tariff HistoryPDF". (3) The OUTER TOC skip in the walker is now keyed on id="how-toc-anchor" only — the new INNER Section-9 TOC (#how-tariffs-toc-anchor) is walked normally so it appears in the Full Guide PDF as part of Section 9 just like every other sub-block. (4) The route-h3 / route-body / changelog-related button classes (ug-all-tariffs-pdf-btn, ug-tariff-hist-btn, ug-cl-big-btn) are added to the _ugPdfEmitNode skip list so they're never emitted as standalone elements either. Result: clicking "Full Guide PDF" now emits the full multi-page User Guide including all six route sub-sections, and clicking "ALL TARIFFs PDF" / per-route "PDF" buttons can no longer be derailed by a single problem element either. (5) Version bump: every B2.5 user-facing reference replaced with B2.6 — title bar (GB-BizCalc - B2.6), User Guide pill stamp + modal header stamp, version-badge ("Version B2.6"), all PDF headers/footers, save-as filenames (tariff-analysis-*-B2.6.pdf, mfr-cost-analysis-*-B2.6.pdf, roi-*-B2.6.pdf, ship-calc-*-B2.6.pdf, trade-terms-chart-B2.6.pdf, gb-bizcalc-user-guide-*-B2.6.pdf, gb-bizcalc-changelog-*-B2.6.pdf, gb-bizcalc-user-guide-tariffs-*-B2.6.pdf, gb-bizcalc-user-guide-all-tariffs-B2.6.pdf, gb-tariff-history-B2.6.pdf), Save-a-copy HTML download "GB-BizCalc - B2.6 (with-data).html". Internal VERSION constants (TariffCalc + MFR-CostCalc) updated to 'B2.6'. Historical "B2.5 #N" / "B2.5 PASS-N" code-comment markers and the B2.5 changelog row narrative are intentionally preserved as-is — they document when each feature originally shipped. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Tariff calculation paths for U.S. / Canada / Mexico imports are byte-for-byte unchanged.
B2.5Apr 2026Four changes on top of B2.1 (B2.2 was skipped — version numbering jumped straight to B2.5). (1) Header-layout regression from the B2.1 hotfix is resolved. The B2.1 #H1 fix had widened the centered middle flex column (min-width:240px + justify-content:center) to keep Save Data ⤓.json / Load Data ⤒.json on one row, but the new 4th Ship-Calc tab had already widened the LEFT column and the RIGHT stack (GB-CALC/LEADS/BUILD NOTES/Update Tariffs/API Key/Cost Per Click) carried flex-shrink:0 — so the flex container's combined minimum width exceeded the viewport at normal window sizes and the right-hand cluster of readouts spilled outside the panel/tab frames. Fix: the Save Data / Load Data pair (and its hidden <input id="gbBizCalcImportInput">) were moved back into the logo row right after the "User Guide" pill — the exact location they occupied pre-A9.8 #7. The centered middle column was then REMOVED in favour of a single zero-content spacer <div style="flex:1"></div> so the LEFT (logo+tabs) column anchors to the far-left edge and the RIGHT stack anchors to the far-right edge at all viewport widths. All five right-hand elements (GB-CALC / LEADS / BUILD NOTES / Update Tariffs / API key readout / Cost Per Click) now sit inside the panel frame again. (2) Added a big "Download PDF" button pinned to the far right of Pre-Paid Ship-Calc's "1 · Select Customer Card + Profile + Macro Cost Basis" heading. It reuses the existing .ug-pdf-btn-lg orange-pill class (background #f4834f / white font / #d9723f border), sits as tall as the section heading row it's in, and is wired to a new exportShipAllPDF() function that DOM-scrapes all four rendered .ship-scale-card panels and emits a single multi-page pt-unit letter-sized PDF. The PDF opens with a navy #1b3f72 title bar ("Pre-Paid Ship-Calc" / "Freight-adjusted GripBlades margins"), the generated-timestamp + version B2.5 line, a Configuration block (Customer Card / Sub Profile / Macro Cost Basis / Pack Size / MC Qty Committed / EST. SHIP PRICE / context), then each of the four scale cards (N MC Order / 1 MC / 1 Pack / 1 Blade) rendered with title + sub + red ship-allocation line, followed by the GripBlades Margin (vs DIST. price) section's four A/B/C/D-DIST rows with their WAS / drop lines, then the GripBlades ROI FROM MSRP section's three A/B/C-MSRP rows. Every page gets the standard Internal Vibe Coded Biz Software footer with the card/basis context + page N of M. Filename: ship-calc-{card}-profile{sub}-{basis}-B2.5.pdf. If the user has not filled in the Ship-Calc inputs yet (no cards rendered), the export aborts with a helpful alert telling them what fields are still missing. (3) Added small orange "PDF" pills to the top-right of every individual .ship-scale-card result panel. Each pill reuses the existing .ug-pdf-btn-sm class (10px orange pill, same .ug-pdf-btn-sm used throughout the User Guide's per-section exports), is floated with float:right + margin:0 0 6px 10px so it sits in the corner without disturbing the card's flex-column header block, and is wired to exportShipSingleCardPDF(idx) where idx is the scale index (0=Total, 1=1 MC, 2=1 Pack, 3=1 Blade) carried on the new data-ship-card-idx attribute. The single-card PDF uses the same _shipBuildPdf() helper as the big Download PDF button but passes [idx] as the card list, so its output is an identical-style PDF that contains only that scale card. Filename gets a -{key} suffix (e.g. ship-calc-my-card-profile1-landed-mc-B2.5.pdf) so single-card and all-cards exports are distinguishable. (4) Version stamp + every downloadable PDF rolled from B2.1 → B2.5 throughout the app — note B2.2 was intentionally skipped at the user's request. Updated: title bar (GB-BizCalc - B2.5), User Guide modal header stamp, version-badge ("Version B2.5"), all existing PDF headers/footers and save-as filenames for TariffCalc (tariff-analysis-*-B2.5.pdf), MFR-CostCalc (mfr-cost-analysis-*-B2.5.pdf + footer "MFR-CostCalc B2.5"), ROI-Calc (roi-*-B2.5.pdf / roi-analysis-*-B2.5.pdf + footer "ROI-Calc B2.5"), Trade Terms (trade-terms-chart-B2.5.pdf), User Guide (gb-bizcalc-user-guide-B2.5.pdf / gb-bizcalc-user-guide-{section}-B2.5.pdf), Changelog (gb-bizcalc-changelog-B2.5.pdf / gb-bizcalc-changelog-{VER}-B2.5.pdf), Tariff History (gb-tariff-history-B2.5.pdf), and the "Save a copy" HTML download filename "GB-BizCalc - B2.5 (with-data).html". The internal VERSION constants (TariffCalc + MFR-CostCalc) both updated to 'B2.5'. The new Ship-Calc PDFs carry "Ship-Calc B2.5" in their page footer to match. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Historical B2.1 references in code comments ("B2.1 #N: …" markers, User Guide body text mentioning "the 4th tab added in B2.1", and the existing B2.1 changelog row) are intentionally preserved as-is — they document when each feature originally shipped. 91 revisions tracked. —— B2.5 CANADA-IMPORT PASS: TariffCalc now supports importing into Canada (in addition to the original U.S. import path), with two new origin+import combinations handled end-to-end. (C1) The Country of Export dropdown gained "United States" (value="US") as a 4th option beside China (PRC) / Hong Kong / Taiwan, and the Country of Import dropdown gained "Canada" (value="CA") as a 2nd option beside United States. Both carry new onchange handlers (onExportCountryChange / onImportCountryChange) that update the live route label and toggle a new FX rate row. (C2) A new Canada-only input row (#caFxRow, hidden by default) appears underneath the existing Qty + Cost + Freight grid whenever Country of Import = Canada. It holds (a) a USD→CAD numeric input (#caFxRate) defaulting to 1.3700 so every tariff row can print a CAD secondary value beside the USD primary, and (b) a live "Import Route" readout (#caRouteLabel) that explicitly names the active regime — e.g. "China (PRC) → Canada — MFN + China Surtax Order (2024)", "United States → Canada — CUSMA preferential + active counter-tariffs", "Hong Kong → Canada — MFN (China Surtax does NOT apply)". (C3) The new buildFallbackCA(product, hts, origin) function mirrors buildFallback() but branches on route: US→CA returns CUSMA preferential 0% base (tag:BASE), three 25% counter-tariffs on U.S.-origin steel / aluminum / non-CUSMA-compliant motor vehicles (tag:SURTAX, basis:'fob') all with ACTIVE status as of April 2026, an inactive-by-default MFN fallback for missing CUSMA cert, plus shared 5% GST (tag:TAX, basis:'duty_paid') and an informational zero-amount CBSA fee (tag:FEE, basis:'flat'); CN/HK/TW→CA returns 7% MFN ad valorem (verified for HTS 8211.92), a 25% China Surtax Order (2024) SOR/2024-187 steel+aluminum surtax that is active:true for CN origin and active:false for HK/TW (with clear language in short_desc/laymens_desc explaining the scope), plus the same shared GST + CBSA rows. Both routes also emit inactive_tariffs: US route includes the Mar 2025 broad consumer-goods counter-tariffs (REMOVED Sep 1 2025) + the initial Apr 2025 auto surtax (SUPERSEDED by the current Motor Vehicles Surtax Order); CN route includes the 100% China EV surtax (REPEALED Mar 1 2026 under the Carney-Beijing quota-for-farm-goods swap, replaced by a 49,000→70,000/yr volume cap) + the proposed-but-unenacted Critical Manufacturing surtax (batteries / semiconductors / solar / critical minerals consultation). Every entry has laymens_desc + legal_detail + political_context so the existing summary popup and "View full legal detail + political context →" modal work with zero UI changes. (C4) 11 new TARIFF_SOURCES arrays for the Canadian tariff IDs (ca_mfn, ca_gst, ca_cbsa_fee, ca_cusma, ca_china_steel_surtax, ca_china_ev_surtax, ca_china_critical, ca_us_steel_surtax, ca_us_alum_surtax, ca_us_auto_surtax, ca_us_retail_consumer) pulling from Canada Gazette Part II (SOR/2024-187 + SOR/2025-267 + Order Amending the China Surtax Order), CBSA Customs Tariff T2025, Dept. of Finance Canada, PwC Canada, EY Tax Alerts, Norton Rose Fulbright, GHY International, and news coverage of the EV surtax repeal — the openLegal modal falls through to TARIFF_SOURCES[t.id] so these sources render automatically in the "TariffCalc Source Data" box of every Canadian tariff's full-detail popup. (C5) runCalc() now reads the Country of Import select and dispatches: imp==='CA' routes through fetchLiveRatesCA (live) / buildFallbackCA (offline) + a namespaced cache key "CA:{origin}:{hts}" so U.S.-import and CA-import results never collide in localStorage; imp==='US' keeps the original path byte-for-byte unchanged. Live fetch failures for Canada gracefully fall back to cache then to buildFallbackCA (instead of bubbling) so the user always sees results. (C6) fetchLiveRatesCA() is a Canada-specific Anthropic + web_search API call that takes the same apiKey + web_search_20250305 tooling as fetchLiveRates but ships a Canadian-customs prompt with branched CURRENT RATE CONTEXT blocks for U.S.-origin vs. China/HK/TW-origin, explicit schema guidance for the new "basis" field values ('fob' / 'duty_paid' / 'flat'), instructions to include inactive_tariffs with status_note and to return Canadian MFN + CUSMA + China Surtax Order + counter-tariff verification for the exact HTS. Response is parsed identically (regex + JSON.parse) and import_country/origin_country/route fields are set defensively. lastFetchUsage is populated so the Cost Per Click readout + click-log keep working for Canadian refreshes at the same cost-tracking fidelity as U.S. refreshes. (C7) renderResults() is now Canada-aware. A new two-pass calc loop computes fob-basis tariffs first (MFN / CUSMA / surtaxes / specific-per-unit), then resolves duty_paid-basis tariffs (GST) as rate% × (FOB + sum of fob duties) so the 5% GST correctly applies on top of duty. basis:'flat' tariffs read flat_amount directly (CBSA fee = $0 informational). The three metrics cards (Tariff / Goods / Landed) + their per-piece sub-lines append " · C$XX.XX" CAD secondary values when isCA using the user's FX rate. Every active tariff row shows a CAD sub-line under the USD primary amount + per-piece cell, and the rate column shows route-appropriate wording ("5% of duty-paid" for GST, "— / informational" for the zero CBSA fee, "25%" for surtaxes, "7%" for MFN). The math-audit box gains two extra Canadian-only lines: "Duty-paid basis (for GST) = $FOB + $duties = $total" + "USD→CAD @ {fx} · Landed in CAD: C${landed*fx}". (C8) Inactive Canadian tariffs render in the existing Non-Active Tariffs section and are clickable exactly like U.S. inactive tariffs — the summary popup (showTT) and full-detail modal (openLegal) both work through the existing code paths because the new entries use the same id/name/laymens_desc/legal_detail/political_context/status_note/last_updated schema. showTT and openLegal's rate-string logic was extended to recognize basis:'flat' (prints "$X flat/entry" or "No CBSA fee — informational only") and basis:'duty_paid' (prints "X% of (FOB + duties)" / "X% of the duty-paid value"). (C9) exportPDF() now branches on data.import_country === 'CA'. The title bar dynamically switches between "US Import Analysis (China Export / HK Export / Taiwan Export)" and "Canadian Import Analysis (China Export / Hong Kong Export / Taiwan Export / U.S. Export)". The generated-timestamp line appends "USD→CAD @ 1.3700" when isCA. The calculation summary gains a "Landed in CAD: C$… · Landed ea/pc CAD: C$…" line plus a "GST applied on duty-paid value = FOB + duties = $…" explainer. Each ACTIVE TARIFFS row reserves an extra 40pt on the right (160pt total instead of 120pt) so a CAD secondary amount can print in 9pt gray directly beneath the 11pt USD primary without crowding long tariff names. Basis:'flat' zero-amount rows render as "informational" instead of "$0.00". Rate-description text uses the same basis-aware wording as the on-screen row. All other PDF sections (product block, shipping qty, inactive tariffs, AI notes, footer) are unchanged. (C10) No version bump — this is a Canada-import PASS on B2.5 itself, so every filename / PDF footer / HTML-save name / title-bar stamp remains 'B2.5' per user request "B2.5 is fine". No existing data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. U.S.-import behaviour is byte-for-byte unchanged when Country of Import stays at "United States" — the CA branch only activates on explicit user selection. New localStorage keys are namespaced inside the existing CACHE_KEY store so they never collide with pre-existing cached U.S.-import results. —— B2.5 MEXICO-IMPORT + FX-FIELD HOTFIX: This release adds Mexico as a third Country of Import (alongside U.S. and Canada), expands Country of Export to include Canada and Mexico (so MX↔CA, CA↔MX, MX→US routes can all be modeled), and fixes a long-standing visual bug in the new B2.4 USD→CAD FX field. (M1) Country of Import dropdown now offers United States / Canada / Mexico. Country of Export dropdown now offers China (PRC) / Hong Kong / Taiwan / United States / Canada / Mexico — six origins. Together they unlock USA→Mexico, Canada→Mexico, China→Mexico, HK→Mexico, TW→Mexico routes plus the reciprocal Mexico→US, Mexico→Canada flows. (M2) New #mxFxRow input row mirrors #caFxRow exactly: a USD→MXN numeric input (#mxFxRate) defaulting to 17.5000 plus a live #mxRouteLabel readout naming the active regime (e.g. "United States → Mexico — USMCA preferential (T-MEC) + IVA 16%", "China (PRC) → Mexico — MFN + IEPS 35% (Aug 2025 ag/textile/footwear order) + IVA 16%", "Hong Kong → Mexico — MFN + IVA 16% (China-specific IEPS 35% does NOT apply)"). The Mexico FX row only appears when Country of Import = Mexico — Canada FX row only appears when Country of Import = Canada — both are hidden in the default United States configuration. (M3) New buildFallbackMX(product, hts, origin) builds a complete Mexican tariff stack analogous to buildFallbackCA: USMCA/T-MEC preferential 0% base for U.S. and Canadian origin (tag:'BASE', basis:'fob'); MFN TIGIE 2025 fallback (15% for HTS 8211.92, marked active:false by default since T-MEC certification covers 99% of cases); IVA 16% on duty-paid value (tag:'TAX', basis:'duty_paid'); DTA Derecho de Trámite Aduanero 0.176% of customs value (tag:'FEE', basis:'fob'); a placeholder PRODECON entry explaining the agente aduanal model with flat_amount:0. China-route variant (CN→MX) replaces the T-MEC base with MFN 15% + IEPS Emergency Surcharge 35% (Aug 2025 DOF decree on textiles/footwear/steel/auto), with the IEPS active flag set false for HK/TW since the decree targets Chinese-origin specifically. inactive_tariffs include the proposed-but-not-enacted Sep 2025 50% Chinese auto tariff and the cuotas compensatorias anti-dumping framework so users can see what's NOT applying alongside what IS. (M4) New fetchLiveRatesMX(product, hts, origin) extends the AI-refresh path with Mexico-specific CURRENT RATE CONTEXT: branched messaging for USMCA-origin (US/CA → MX) vs. non-FTA-origin (CN/HK/TW → MX), explicit verification asks for SAT/SHCP regulations, T-MEC Article 4.7 (rules of origin), TIGIE 2025 MFN per HTS, IEPS Aug 2025 emergency decree annex coverage, IVA 16%, DTA, anti-dumping cuotas via the UPCI database. Sets import_country='MX' and parsed.route='USCA_TO_MX' or 'CN_TO_MX' defensively before returning. Schema matches buildFallbackMX so the existing render + PDF code can consume either output identically. (M5) runCalc() route dispatch now has a third branch: if importCountry='MX', call fetchLiveRatesMX → cache 'MX:origin:hts' → render. Falls back to buildFallbackMX on offline or API errors. The cache namespace prevents Mexico results from colliding with Canadian or U.S. results in localStorage. (M6) renderResults() generalized: the previous CA-only flags (isCA / fx / fmtCAD / withCAD) became unified hasSec / secCode / secSym / fx / fmtSec / withSec drivers that work for both Canadian (CAD) and Mexican (MXN) imports. The dual-currency tariff-row display, the per-piece + total amount columns, the metric badges (Total Tariffs, Goods, Landed Cost), and the math-audit footer all show MXN secondary when import=MX. The audit's "Duty-paid basis (for IVA)" label replaces the Canadian "(for GST)" label automatically — same calculation logic, just relabeled. (M7) exportPDF() PDF export branched the same way. Title bar reads "TariffCalc — Mexican Import Analysis (USA Export)" / "(China Export)" etc. Header timestamp line includes "USD→MXN @ 17.5000" when import=MX. Per-tariff rows show fmtSec(amount × fx) in 9pt gray directly beneath the USD primary. Calculation summary adds "Landed in MXN: Mex$..." and "IVA applied on duty-paid value..." lines. Filename remains tariff-analysis-{product}-B2.5.pdf — same base name across all import regimes since the title bar already discloses the destination country. (M8) Hotfix: the B2.4 #2 USD→CAD FX field used the existing .pfx CSS class (designed for a single "$" prefix character) which positioned the "1 USD =" prefix span at left:13px while the input had only padding-left:26px — meaning "1 USD =" (which is ~50px wide at 11px mono) overlapped the input's value text creating the "1 1.3700" double-display visible in the user's screenshot. Replaced both #caFxRow and #mxFxRow's prefix wrapper with a clean flexbox layout: a left-aligned span containing "1 USD =" with white-space:nowrap and flex-shrink:0, plus an input set to flex:1 + min-width:0 — so the prefix and input are siblings sharing horizontal space rather than overlapping in absolute positioning. The visual result is identical to the .pfx pattern but without the overlap risk regardless of prefix-text length. (M9) Version bump: every B2.4 reference in the file replaced with B2.5 — title bar (GB-BizCalc - B2.5), User Guide modal stamp, version-badge, all PDF headers/footers, save-as filenames (tariff-analysis-*-B2.5.pdf, mfr-cost-analysis-*-B2.5.pdf, roi-*-B2.5.pdf, ship-calc-*-B2.5.pdf, trade-terms-chart-B2.5.pdf, gb-bizcalc-user-guide-*-B2.5.pdf, gb-bizcalc-changelog-*-B2.5.pdf, gb-tariff-history-B2.5.pdf), Save-a-copy HTML download "GB-BizCalc - B2.5 (with-data).html". Internal VERSION constants (TariffCalc + MFR-CostCalc) updated. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. U.S.-import + Canada-import behaviour is byte-for-byte unchanged when the user keeps Country of Import on those values — the Mexico branch only activates on explicit selection. —— B2.5 PASS-3 USER-GUIDE TARIFF EXPLAINER OVERHAUL: Section 8 ("Tariff Calculation Engine") and Section 9 ("Each Tariff Explained") of the User Guide were rebuilt to cover all six supported import-route combinations end-to-end. (P3-1) Section 8 now opens with a U.S.-rules header (preserving the original CBP wording), then a "Canadian Imports — CBSA Rules (two-pass calc)" subhead explaining how Pass 1 sums fob-basis tariffs and Pass 2 resolves the 5% federal GST as rate% × (FOB + sum-of-fob-duties), plus a "Mexican Imports — SAT / SHCP Rules (two-pass calc)" subhead detailing the same two-pass mechanic for IVA 16%, the DTA 0.176% FOB-basis fee, the IEPS Aug 2025 emergency surcharge for Chinese-origin goods, and the agente-aduanal requirement. The closing "Universal invariant" paragraph documents the basis field ('fob' / 'duty_paid' / 'flat') so future tariff additions are pure data changes. (P3-2) Section 9 is now route-keyed. The h2 lost its "GB - Tariff History" + "PDF" buttons; in their place sits a single big orange-on-white-font "ALL TARIFFs PDF" button (.ug-all-tariffs-pdf-btn) that emits a TOC-front-loaded multi-page PDF containing every route's tariff explanations in one document. Below the h2 is a new inner Table of Contents (.how-toc with id="how-tariffs-toc-anchor") listing six clickable jumps: 9.1 USA Import (CHINA Export), 9.2 Canada Import (CHINA Export), 9.3 Canada Import (USA Export), 9.4 Mexico Import (CHINA Export), 9.5 USA Import (MEXICO Export), 9.6 Mexico Import (USA Export). (P3-3) Every sub-section uses a new .ug-route-h3 styling: 18px blue (#1d4f9c) font with a soft cbdcf3 underline, and each carries its own "GB - Tariff History" + "PDF" buttons floated to the right of the heading. The original Section 9 content (MFN ad-valorem, MFN specific, Section 301, Section 122, MPF, HMF, plus the IEEPA / fentanyl / de-minimis non-active block) is preserved verbatim under 9.1 — that is the same data the section had before, just keyed to route US:CN with the buttons moved into the sub-heading. (P3-4) Each sub-section's body is a <div data-route="IMP:ORIGIN"> populated by a new ugRenderRoutePanel(routeKey, data) function that consumes the same tariff JSON shape returned by fetchLiveRates / fetchLiveRatesCA / fetchLiveRatesMX or by buildFallback / buildFallbackCA / buildFallbackMX. On DOMContentLoaded a new ugRenderAllRoutes() walks all six route keys and populates each panel from its offline baseline, so users see populated content immediately even before clicking Update Tariffs. ugBuildRouteBaseline() reads the user's HTS field if present (defaulting to 8211.92.4060) so panels reflect the active HTS context. (P3-5) Each tariff <h3> inside a route panel is now clickable (cursor:pointer, hover blue underline, ›-chevron suffix). Click handler ugShowTariffDetailRoute(event, routeKey, id, isInactive) pulls the tariff record from UG_ROUTE_DATA[routeKey] (or rebuilds the fallback if not yet rendered) and opens the existing legalBg modal — the same full-detail view used by Tariff-Calc result rows — populated with the lay summary, legal basis, current rate (basis-aware: 'X% of FOB', 'X% of duty-paid', '$X flat/entry', or specific-per-piece), status note (for inactive entries), last-updated stamp, political context box, and Sources box pulling from t.sources or TARIFF_SOURCES[t.id]. The modal is reparented to <body> on open so it paints above the User Guide howBg backdrop. (P3-6) Per-route PDF export — ugExportRoutePDF(routeKey) walks one panel through the existing _ugPdfMake / _ugPdfEmitNode pipeline with a blue 14pt route heading and saves to gb-bizcalc-user-guide-tariffs-{us-cn|ca-cn|ca-us|mx-cn|us-mx|mx-us}-B2.5.pdf. ugExportAllTariffsPDF() emits a 20pt title page, an internal Table of Contents listing all six routes, then each route's body in turn (each with its own blue-underlined sub-heading) into gb-bizcalc-user-guide-all-tariffs-B2.5.pdf. (P3-7) Per-route GB - Tariff History — each panel gets its own per-route localStorage key (gbTariffGuideHistory_US_CN, _CA_CN, _CA_US, _MX_CN, _US_MX, _MX_US) so timeline searches and snapshots are scoped. ugOpenTariffHistoryRoute(routeKey) reuses the existing ugTariffHistoryBg modal but sets UG_ACTIVE_HIST_ROUTE so ugRenderTariffHistoryList() pulls from the matching per-route store and updates the modal title to "GB — Tariff History · {N.X} {Route Label}". For US:CN the route store is merged with the legacy global gbTariffGuideHistory store on the fly so any pre-PASS-3 snapshots stay searchable from 9.1's history button. The legacy section-h2 ugOpenTariffHistory() entry point is preserved (clears UG_ACTIVE_HIST_ROUTE and resets the title) for backward compatibility with any code or test that still calls it. (P3-8) Update Tariffs is now route-aware end-to-end. updateTariffs() reads Country of Import, dispatches to fetchLiveRatesCA / fetchLiveRatesMX / fetchLiveRates accordingly, namespaces the cache key with 'CA:' / 'MX:' / 'US:' prefix to prevent collisions, snapshots BOTH the matching route's history AND the legacy global history (so the original GB-TARIFF-HISTORY entry point keeps working), and re-renders BOTH the legacy ugTariffGuideBody (US:CN compatibility) AND the matching route panel's data-route div. So clicking "Update Tariffs" with Country of Import = Mexico and Country of Export = China now refreshes 9.4's panel, snapshots its prior content into gbTariffGuideHistory_MX_CN, and the explanations + sources update automatically. Same flow for every other (import, origin) pair. (P3-9) New CSS: .ug-all-tariffs-pdf-btn (orange pill, replaces section-h2 hist+pdf), .ug-route-h3 (18px blue blue-underlined sub-heading), .ug-route-body h3[onclick] (clickable cursor + hover state + › chevron). All other styles untouched. (P3-10) Backward compatibility: the original ugRenderTariffGuide(data) and ugSnapshotTariffGuide(note) functions remain in place; they still render into / snapshot from the legacy ugTariffGuideBody div (which is now the data-route="US:CN" div, so they continue updating 9.1 as before). The only behavioural change is that they ALSO have route-aware siblings (ugRenderRoutePanel + ugSnapshotRouteHistory) now active alongside them. No version bump — this is a User Guide PASS on B2.5 itself, so every filename / PDF footer / HTML-save name / title-bar stamp remains 'B2.5'. No data paths touched.
B2.1Apr 2026Two changes on top of B2.0 — a bug fix and a new 4th tab. (1) Fixed the BIG button in the compact Changelog header: clicking it stopped opening the full-screen overlay. Root cause: #ugChangelogFullBg is nested four levels deep inside #panelTariff.app-panel, which carries display:none whenever the Tariff tab is not active — so classList.add('on') set the overlay's own display to flex but the hidden parent panel clipped the whole subtree to nothing on every tab except Tariff. ugOpenChangelogFull() now reparents #ugChangelogFullBg to <body> on every open (same pattern already used by openHow and ugOpenTariffHistory in B1.6), so the overlay escapes the ancestor display:none + any stacking context and reliably paints over the whole app regardless of which tab is active. The SMALL button (added in B2.0) is now verifiable too since BIG opens again. (2) NEW 4th main tab: Pre-Paid Ship-Calc, sitting immediately to the right of ROI-Calc. A Pre-Paid order is one where GripBlades LLC pays the freight to win a large Master-Carton commitment, and Ship-Calc answers the question "how much does that freight drop our GripBlades margin at each scale?". The tool wires a teal-gradient .tab-fold-icon.ship icon into the tab bar, a new #panelShip app-panel with background:#fff (matching ROI-Calc's clean white-frame styling), and three control rows inside the panel: (a) Customer Card + Sub Profile + Macro Cost Basis dropdowns that pull live data out of ROI-Calc's roiCustomerCards / roiCardData / roiBasisLanded stores — no retyping of any price or LANDED $/pc is required; (b) a Pack Size radio pair limited to "10 Pack MC" and "40 Pack MC" only (individual packs are not a Pre-Paid choice); (c) MC Qty + EST. SHIP PRICE numeric inputs plus a Context line that echoes the current Card / Profile / Basis / Pack in plain English. As soon as every required field is filled, a new #shipResultsWrap grid builds four result cards — one per scale (N MC Order Total / 1 MC / 1 Pack / 1 Blade) — each card printing the freight allocation for that scale in red at the top, followed by the four A-DIST / B-DIST / C-DIST / D-DIST GripBlades margin rows and the three A-MSRP / B-MSRP / C-MSRP GripBlades-ROI-from-MSRP rows with the stored PROMO % applied. Under every row a small red "WAS - $X.XX / X.XX% · drop -$X.XX / -X.XX%" line makes the exact freight-induced drop explicit. The formulas mirror ROI-Calc's roiRecalc — landed = LANDED $/PC × pack × mcMult; effMsrp = rawMsrp × (1 - promo/100); adjusted GripBlades margin = price - landed - freightAllocation. Distributor retail-ROI readouts are intentionally omitted since manufacturer-paid freight does not affect distributor-side margins. switchTab() now knows about 'ship' and calls a new shipCalcBuild() on first activation which (re)populates every dropdown from the live ROI-Calc stores. USER GUIDE gets a new section #5 "Pre-Paid Ship-Calc Interface Guide" (anchor #how-ship-interface) and all existing sections #5-#18 were renumbered to #6-#19 so the numbering stays gapless. Version stamp + every downloadable PDF rolled from B2.0 → B2.1 throughout the app (title bar, User Guide header + version badge, TariffCalc / MFR / ROI / Trade-Terms / User Guide / Changelog / Tariff History PDF footers and filenames, and the "Save a copy" HTML download filename "GB-BizCalc - B2.1 (with-data).html"). No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. 90 revisions tracked. —— B2.1 HOTFIX PASS: five same-version tweaks on top of the B2.1 ship. (H1) The "Save Data ⤓.json" / "Load Data ⤒.json" pair in the compact header row had wrapped onto two stacked rows and drifted right because the new 4th Ship-Calc tab widened the left column and squeezed the middle flex column below its previous breathing room. Fixed by restructuring that middle column's inline style — flex-wrap flipped from 'wrap' to 'nowrap', min-width bumped from 12px to 240px, and each button pinned with white-space:nowrap;flex-shrink:0 — so the two buttons sit side-by-side on ONE row, centered between the "USER GUIDE" button on their left and the "GB-Calc" ticker button on their right, exactly where they lived before the Ship-Calc tab existed. (H2) The top-bar red "Breaking" ticker label has been renamed to "Tariff-News" — edit is to the single <div class="ticker-label"> text node inside #topTickerBar (line ~1787). Because .ticker-label carries text-transform:uppercase + letter-spacing:.1em it renders as "TARIFF-NEWS" in the same red pill with no CSS change required; white-space:nowrap already prevents wrap. (H3) A "Cost Per Click" word link has been added next to the ↻ Refresh button inside the TariffNews panel header. It matches the existing Update-Tariffs "Cost Per Click" label exactly in styling (10px accent-color underlined mono text, pointer cursor, hover tooltip "Click to see full Tariff-News Refresh Cost Per Click history with charts") and pairs with a small #newsCostPerClickLastValue readout that shows the cost of the most recent successful refresh. Because Tariff-News Refresh uses the SAME paid Anthropic API + web_search tool as Update Tariffs, every click costs real money — tracked separately in a new localStorage log NEWS_COST_LOG_KEY = 'gbBizCalcNewsRefreshClickLog_v1' so per-source spend is distinguishable. New helpers getNewsClickLog / addNewsClickLog / setNewsCostPerClickReadout mirror the Update-Tariffs pattern, and a new lastNewsFetchUsage variable is populated inside loadNews from response.usage. After every userInitiated successful refresh, loadNews now calls computeCostFromUsage(lastNewsFetchUsage), formats the USD cost, updates the readout in green, and appends {ts, costUsd, inputTok, outputTok, searches, source} to the news log. On page load, the most recent news-log entry's cost is restored to the readout so users see "$0.0162" style info alongside the label even before they click again. The shared #costHistoryBg modal now accepts a source argument — openCostHistory('news') sets _costHistoryCurrentSource = 'news' before rendering, and renderCostHistory / exportCostHistoryPDF / clearCostHistory all branch on that state to read from currentCostLog() (either getClickLog or getNewsClickLog). The modal subtitle updates dynamically to "Every Tariff-News Refresh API call" vs. "Every Update Tariffs API call", the empty-state message and clear-confirm prompt both use the source-specific label, and the PDF export's header banner + page footer + save filename all adapt ("cost-per-click-tariff-news-history-B2.1.pdf" vs. the legacy "cost-per-click-history-B2.1.pdf"). The existing Update-Tariffs label's onclick was changed from openCostHistory() to openCostHistory('update') so the two sources never leak state. (H4) Every freight-adjusted drop line in Ship-Calc now reads "WAS - $X.XX / Y.YY% · drop -$X.XX / -Y.YY%" instead of "was $X.XX / Y.YY% · drop ...". Uppercase WAS + " - " separator, in renderShipRow()'s template string; the User Guide description in §5 was updated to match the new casing so the documentation and actual render stay in sync. (H5) The section divider between each DIST section's last drop line and the following "GRIPBLADES ROI FROM MSRP (DIRECT-TO-END-USER)" heading was upgraded from a 1px dashed border-top to a solid 4px light-gray (#e5e5ea) bar rendered via a ::before pseudo-element on .ship-row-section-label. The :first-of-type rule (applied to the DIST heading at the top of each card) hides its pseudo-bar so the very first heading in the card doesn't carry a dangling divider. The label itself gained margin-top:14px and padding-top:10px so the bar sits with comfortable breathing room above the heading text. No version bump — this is a hotfix pass to B2.1 itself, so all B2.1 filenames / PDF footers / HTML-save name / title-bar stamps remain 'B2.1'. No data paths touched.
B2.0Apr 2026Two targeted Changelog UX polish fixes on top of B1.9, both centred on the full-screen Changelog overlay inside the User Guide. (1) A new blue "SMALL" pill button (.ug-cl-small-btn — same var(--accent) #0071e3 background + #0061c2 border + white 700-weight 10px uppercase font as the BIG button) has been added to the big-view header's left group, sitting immediately to the right of the orange "PDF" pill that lives to the right of the "CHANGELOG — FULL HISTORY" h3. Clicking SMALL calls ugCloseChangelogFull() — the exact same close path the round "✕" button in the far-right cell already uses — so users get a second, more discoverable way to collapse the overlay back to its compact size inside the User Guide. event.stopPropagation() is called on the button's onclick so the click never bubbles to the backdrop's click-outside-to-close handler, and the SMALL / BIG label pair now reads as a visual mode-toggle: BIG in the compact header expands, SMALL in the full-screen header collapses. (2) Fixed the per-version PDF pill (.ug-cl-row-pdf-btn, added in B1.9 #4) being way too tall. Root cause: .changelog-row is display:flex with default align-items:stretch, so every direct flex-child (including the newly-injected button) was being stretched to match the tallest sibling — which is the long .changelog-note text block. Fix applied by adding align-self:flex-start;flex:0 0 auto;height:16px;max-height:16px;min-height:0 plus line-height:1.2 to the button's rule so the button pins to the top-left corner of the row, next to the .changelog-ver span, and occupies a single 16px-tall rectangular pill instead of spanning the full note height. This applies automatically in BOTH the compact Changelog in the User Guide AND the full-screen Big view (since ugOpenChangelogFull clones #changelogScroll.innerHTML — the buttons ride along with the row markup and inherit the same CSS). The button retains its orange colour, 9px font, 4px border-radius, and ugExportSingleChangelogRowPDF(version) click wiring; all changelog rows (B0.0 through B2.0) now show a compact one-row PDF pill next to the version tag. No data paths changed — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Version stamp + every downloadable PDF rolled from B1.9 → B2.0 throughout the app (title bar, User Guide header + version badge, TariffCalc / MFR / ROI / Trade-Terms / User Guide / Changelog / Tariff History PDF footers and filenames, and the "Save a copy" HTML download filename "GB-BizCalc - B2.0 (with-data).html"). 89 revisions tracked.
B1.9Apr 2026Four coordinated Changelog-UI fixes on top of B1.8, all centred on the User Guide's Changelog block. (1) The B1.4 #4 "click anywhere on the compact Changelog to open the full-screen overlay" behaviour is GONE. The <div id="ugChangelogBox"> no longer carries class="ug-cl-clickable" or onclick="ugMaybeOpenChangelogFull(event)", and a new solid-blue "BIG" pill button (.ug-cl-big-btn — background var(--accent) #0071e3 + #0061c2 border, white 700-weight 10px uppercase font, matching the "GB - Tariff History" launcher pattern) has been added to the compact Changelog header immediately to the right of the orange "PDF" pill. Clicking BIG is now the ONLY way to open the full-screen overlay. event.stopPropagation() is still called on both PDF and BIG so the clicks can never bubble anywhere unwanted. (2) The full-screen Changelog header (.ug-cl-full-head) was restructured into three flex zones — LEFT: "CHANGELOG — FULL HISTORY" h3 + a new orange "PDF" pill grouped in a new .ug-cl-full-left helper; CENTER: a full CMD+F-style search widget (.ug-cl-search reused, new ID suffix …Big so its state is independent of the compact search); RIGHT: the round "✕" close button unchanged. The search widget in the big view has its own _ugClMatchesBig / _ugClMatchIdxBig state, its own ugChangelogSearchBig / ugChangelogSearchStepBig / ugChangelogSearchKeyBig handlers, its own ◀/▶ arrow buttons, its own match counter, and runs the same version-number regex (/^[A-Za-z]\d+\.\d+$/) + phrase-walker semantics as the compact-view search; Enter/Shift+Enter step through matches and Escape clears. ugOpenChangelogFull now resets _ugClMatchesBig, _ugClMatchIdxBig, the big input value, and the big counter on every open so the overlay starts clean. (3) The new orange "PDF" pill beside the big-view heading calls the same ugExportChangelogPDF() used by the compact header — clicking it downloads the ENTIRE changelog history as gb-bizcalc-changelog-B1.9.pdf. (4) A small orange "PDF" pill (.ug-cl-row-pdf-btn — 9px font, padding:1px 7px, 4px border-radius — visibly a sub-affordance smaller than the section pills) is now injected automatically at DOMContentLoaded right after every .changelog-ver span inside the compact #changelogScroll. A new ugInstallPerVersionPdfButtons() walks every .changelog-row, reads the version text, builds the button with data-cl-ver=VER and a click handler that calls ugExportSingleChangelogRowPDF(VER), and splices it in after the version span. Double-install is guarded by a querySelector check. Because ugOpenChangelogFull() clones #changelogScroll.innerHTML into #ugChangelogFullList, these per-row buttons automatically appear in the big view too without any separate big-view hook. The new ugExportSingleChangelogRowPDF(version) function finds the matching row in either view, builds a jsPDF document with a "GB-BizCalc — Changelog {VER}" title block, "Single revision export · App version B1.9" subtitle, a version+date header line, and the note body walked through _ugPdfPara, then saves it as gb-bizcalc-changelog-{VER}-B1.9.pdf (e.g. gb-bizcalc-changelog-B1.5-B1.9.pdf). All historical changelog rows (B0.0 through B1.8) are preserved in place and gain per-row PDF pills automatically. The big-view search's text-node walker skips any text inside <button> elements so the per-row PDF pills and the close button never match. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Version stamp + every downloadable PDF rolled from B1.8 → B1.9 throughout the app (title bar, User Guide header + version badge, TariffCalc / MFR / ROI / Trade-Terms / User Guide / Changelog / Tariff History PDF footers and filenames, and the "Save a copy" HTML download filename "GB-BizCalc - B1.9 (with-data).html"). 88 revisions tracked.
B1.8Apr 2026One targeted change on top of B1.7. The User Guide's click-to-expand Changelog popup (.ug-cl-full-bg / .ug-cl-full) has been re-sized from the B1.5 centered card (max-width:760px × fixed body height 528px) back into a true full-screen overlay so users can see many more rows at once while searching. The backdrop now uses align-items:stretch;justify-content:stretch with a tiny 0.5rem gutter, the panel drops its max-width:760px / max-height:calc(100vh - 4rem) caps and switches to width:100%;height:100%;max-width:none;max-height:none, and the body drops the hard 528px height in favour of flex:1 1 auto + min-height:0 so it expands to fill the remainder of the panel and scrolls inside itself. The header is now flex-shrink:0 so the title + close button stay pinned to the top while the body scrolls underneath, and the close button was bumped from 26×26 to 28×28 at 14px font to match the larger frame. Backdrop dimmer nudged from rgba(0,0,0,.45) to rgba(0,0,0,.55) for slightly better contrast against the brighter panel. The round "X" close button, click-outside-to-close backdrop, and Escape-to-close handling all continue to work unchanged. Opens from the same click-anywhere-on-the-compact-Changelog interaction (ugMaybeOpenChangelogFull → ugOpenChangelogFull), and the CMD+F-style search widget in the compact header still drives the same row-highlight behaviour in the expanded view. No data paths changed — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved untouched. Version stamp + every downloadable PDF rolled from B1.7 → B1.8 throughout the app (title bar, User Guide header + version badge, TariffCalc / MFR / ROI / Trade-Terms / User Guide / Changelog / Tariff History PDF footers and filenames, and the "Save a copy" HTML download filename). 87 revisions tracked.
B1.7Apr 2026User Guide restructure on top of B1.6. Two new interface-guide sections added and all subsequent sections shifted down by two. (1) New Section 2 — "MFR-CostCalc Interface Guide" (anchor #how-mfr-interface) — walks through the Blade Cost Calculator tab: the Completed-Blade vs. Blade-Component entry-mode toggle, the 10 Pack / 40 Pack pack selector and how it unlocks the MFR Pack Cost field, the stamped-blade / PVC-grip component inputs and auto-summed total, the MC Cardboard cost row with per-blade read-outs, the dual Cost Reference Chart switcher (MFR Cost Ref. Chart ◀ ▶ LANDED Cost Ref. Chart) including per-cell Last-Updated stamps and the orange PDF export (dual-page when LANDED is active), and the four output metrics (Total MFR Cost / Blade, Cost per Pack, Cost per MC, Cost per Pallet) plus the Itemized MFR Cost Breakdown. (2) New Section 3 — "Tariff-Calc Interface Guide" (anchor #how-tariff-interface) — explains the SHIPPING REFERENCE chart in depth for both pack configs. For the 10 Pack config: 120 Packs/MC, 45 MC/Pallet, 5,400 Packs/Pallet, 54,000 pcs/Pallet, TEU 20ft = 24 pallets/129,600 packs/1,296,000 pcs, FEU 40ft = 48 pallets/259,200 packs/2,592,000 pcs. For the 40 Pack config: 24 Packs/MC, 54 MC/Pallet, 1,296 Packs/Pallet, 51,840 pcs/Pallet, TEU 20ft = 26 pallets/33,696 packs/1,347,840 pcs, FEU 40ft = 52 pallets/67,392 packs/2,695,680 pcs. The section then documents how those numbers drive the Pallets / TEU / FEU popup's live hint and the Quantity auto-fill (selectPack → openShipPopup → recalcPieces chain), and closes by pointing out the cross-tab wiring into ROI-Calc: the same PACK_DATA values are used as ROI-Calc's mcMult (120 for 10 Pack MC, 24 for 40 Pack MC), ROI_PACKS_PER_MC ({10:120, 40:24}), and the three built-in cost bases Pallet/TEU/FEU. (3) The previously-section-2 ROI-Calc Interface Guide is now Section 4 (anchor #how-roi-interface unchanged) and carries a new "Where the pack-size multipliers come from (SHIPPING REFERENCE link)" subsection that elaborates the same data-flow detail from the ROI-Calc side and cross-references Section 3. (4) All remaining sections shifted +2 so numbering now reads: 1. The GripBlades™ Inventor Is Awesome, 2. MFR-CostCalc Interface Guide, 3. Tariff-Calc Interface Guide, 4. ROI-Calc Interface Guide, 5. Overview & Purpose, 6. Inputs — What You Enter, 7. Tariff Calculation Engine, 8. Each Tariff Explained, 9. Live Rate Lookup (AI + Web Search), 10. Offline Mode & Caching, 11. News Feed, 12. HTS Code Lookup Tools, 13. Source Transparency, 14. PDF & Excel Export, 15. Limitations & Disclaimer, 16. Internet Exposure & Privacy, 17. API Keys — Risks & Solutions, 18. Where Your Data Lives & Cross-Browser Saving. All 16 existing anchor IDs (how-inventor, how-roi-interface, how-overview, how-inputs, how-calc, how-tariffs, how-live, how-offline, how-news, how-hts, how-sources, how-exports, how-limits, how-exposure, how-apikeys, how-saving) are unchanged so every deep-link, every Tariff-History snapshot reference, and the user-guide-{anchorId}-BX.Y.pdf filename convention all still resolve. TOC updated to list all 18 entries in the new order. No data paths touched — every MFR COST REF. CHART entry, LANDED COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, Build Notes entry, and Tariff History snapshot is preserved exactly as-is. Version stamp + every downloadable PDF rolled from B1.6 → B1.7 throughout the app (title bar, User Guide header + version badge, TariffCalc / MFR / ROI / Trade-Terms / User Guide / Changelog / Tariff History PDF footers and filenames, and the "Save a copy" HTML download filename). 86 revisions tracked.
B1.6Apr 2026One targeted fix on top of B1.5. Bug: clicking the blue "GB - Tariff History" button from inside the User Guide (at the h2 header of section 6, "Each Tariff Explained") opened the Tariff History popup but it was not visible — the popup appeared to do nothing. Only by closing the User Guide and navigating to the Tariff-Calc tab did the popup reveal itself, sitting open on top of the TariffCalc interface. Cause: openHow() reparents the User Guide modal (#howBg) to be the last child of <body> so it escapes any ancestor stacking context. That reparenting placed #howBg AFTER #ugTariffHistoryBg in document order. Although the Tariff History popup's z-index (2147483100) is nominally higher than the User Guide's (2147483000), in some render paths (the User Guide's sticky header and scrollable overflow container form a second stacking surface, and certain browsers resolve the resulting compositing order in DOM-last-wins fashion when the active scroll container is a fixed sibling), the popup was painting underneath the User Guide backdrop rather than above it. Fix: ugOpenTariffHistory() now reparents #ugTariffHistoryBg to document.body on every open (making it the last child of <body>, unambiguously after #howBg in DOM order), AND locks document.body.style.overflow = 'hidden' for the duration of the popup — mirroring the exact pattern used by the full-page Changelog popup (ugOpenChangelogFull) which has never had this glitch. ugCloseTariffHistory() releases that scroll lock only if no other full-screen overlay (User Guide or Changelog Full) is still open, so stacking behaviour stays correct across nested opens. The popup now opens right there inside the User Guide, centered above section 6, exactly where the user clicked the button — regardless of whether the User Guide itself is open or closed. No data paths changed — every saved Tariff History snapshot, MFR COST REF. CHART entry, ROI-Calc profile, MACRO COST BASIS row, CUSTOMER CARD profile, Leads entry, and Build Notes entry is preserved untouched. Version stamp + every downloadable PDF rolled from B1.5 → B1.6 throughout the app (title bar, User Guide header + version badge, TariffCalc / MFR / ROI / Trade-Terms / User Guide / Changelog / Tariff History PDF footers and filenames, and the "Save a copy" HTML download filename).
B1.5Apr 2026Five User-Guide and Tariff-section fixes on top of B1.4. (1) Fixed the Full Guide PDF — previously it produced only ~2 pages because the body walker recursed into each top-level element's CHILDREN instead of dispatching on the element itself, so direct h2/p/ul siblings emitted nothing. Introduced a new _ugPdfEmitNode(ctx, node) helper that handles a single node based on tag (H2/H3/P/UL/OL/LI/CODE/PRE/A/DIV/SECTION), wired ugExportFullGuidePDF to call it for every top-level child of #howBody, and also added an explicit "Table of Contents" rendering at the top of the PDF (pulled live from #how-toc-anchor with all 16 anchor labels, indented, accent color). A clear "CHANGELOG — See In-App PDF Download for CHANGELOG FILE." note now appears immediately under the title block (italic, gray) and the changelog wrapper is skipped entirely during the body walk. ugExportSectionPDF was simplified to delegate its per-sibling dispatch to the same _ugPdfEmitNode helper so sections and the full guide share identical formatting. (2) The Changelog click-to-expand overlay was re-sized from full-page-takeover (max-height:calc(100vh - 4rem), align-items:flex-start) to a centered popup card (align-items:center, max-width:760px, fixed body height of 528px which is roughly 3× the 176px compact changelog). The backdrop dimmer was softened from rgba(0,0,0,.65) to rgba(0,0,0,.45) so the User Guide stays visible behind the popup. The round "✕" close button, the click-outside-to-close backdrop, and ESC all continue to work unchanged. (3) The Non-Active Tariffs subsection of User Guide §6 "Each Tariff Explained" now enumerates each currently non-active tariff — IEEPA Reciprocal (orig. 125%, struck down SCOTUS Feb 20, 2026), IEEPA Fentanyl (originally 20%, reduced to 10% under Nov 2025 US-China deal, then struck down with the rest of the IEEPA stack), and De Minimis Exemption Suspension ($800 threshold suspended for China, maintained post-SCOTUS, only affects small-parcel e-commerce not bulk imports) — each with its own h3 label (smaller gray styling to distinguish from active tariffs) and a full plain-English description including the legal basis and current status. (4) The body of §6 is now rendered dynamically from tariff JSON. The hardcoded prose lives inside a new <div id="ugTariffGuideBody"> wrapper; on DOMContentLoaded we call ugRenderTariffGuide(buildFallback('general import','8211.92.4060')) with a shape-normalization step (.tariffs with .active flags → .active_tariffs), and on every successful UPDATE TARIFFS click inside updateTariffs() we first snapshot the current body via ugSnapshotTariffGuide(note) then call ugRenderTariffGuide(liveData) with the fresh data. This keeps the section permanently in sync with whatever tariffs are actually in force — if UPDATE TARIFFS discovers a new active tariff or moves an old one to Non-Active, §6 reflects that immediately. The renderer emits rate pills (X% / $Y/pc) in the h3 when rate_pct / specific_rate are non-zero, italic legal_detail tails in active entries, italic status_note tails in inactive entries, and a final "Last synced from live tariff data: …" timestamp paragraph. (5) Added the "GB - Tariff History" feature — a new blue pill button (solid var(--accent) #0071e3 background, white 700-weight 10px font, matching the "Back to Table of Contents" hue but as a filled button) sits immediately left of the orange PDF button in the §6 h2. Clicking it opens a new popup (.ug-th-bg / .ug-th-panel) whose header contains: a left-anchored "GB - Tariff History" title, a centered CMD+F-style search box (.ug-th-search, 300px max-width, searches tariff name, note text, timestamp, and snapshot body text with live-filtered rendering and .ug-th-mark yellow highlights), a small orange "PDF" pill (ugExportTariffHistoryPDF — renders every history entry with a bold blue timestamp, italic gray note, and the snapshot body walked through _ugPdfEmitNode so styling matches the rest of the app, saved as gb-tariff-history-B1.5.pdf), and a round "✕" close button (28×28). History is persisted to localStorage key 'gbTariffGuideHistory' capped at 50 most-recent entries, each with an ISO timestamp and an origin-of-change note string. Every time UPDATE TARIFFS runs successfully, ugSnapshotTariffGuide is called with a note like "Live update — HTS 8211.92.4060, CN" BEFORE the body is overwritten, so the prior version is always preserved and browseable. An empty-state message appears when no history has been captured yet. Version bumped to B1.5 everywhere (title bar, User Guide header + modal, version badge, VERSION const × 2, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B1.5.pdf, mfr-cost-analysis-B1.5.pdf, roi-…-B1.5.pdf, cost-per-click-history-B1.5.pdf, trade-terms-chart-B1.5.pdf, gb-bizcalc-user-guide-B1.5.pdf, gb-bizcalc-user-guide-{section}-B1.5.pdf, gb-bizcalc-changelog-B1.5.pdf, gb-tariff-history-B1.5.pdf — and the saved HTML filename "GB-BizCalc - B1.5 (with-data).html"). 86 revisions tracked.
B1.4Apr 2026Five User-Guide PDF / Changelog-ergonomics fixes on top of B1.3. (1) A new orange "Full Guide PDF" button was added to the User Guide's modal header, immediately to the left of the "X" close button, using the same orange-pill styling (#f4834f background / white font / #d9723f border) as every other download pill in the app. Clicking it runs ugExportFullGuidePDF(), which walks #howBody's top-level children, skips the Table-of-Contents block, and renders each h2/h3/p/ul/li/code node into a pt-unit letter-size jsPDF document (helvetica, 0.75" margins, per-section headings at 16pt bold with a pre-gap, paragraphs at 10pt, bullets with a manual • + 10pt/14pt wrap). When the walker hits the wrapper that contains #ugChangelogBox, the ENTIRE changelog section is replaced by a one-line placeholder "ChangeLog — see inside the app." so the exported PDF stays evergreen and never bloats with 85 revision entries. Output is saved as gb-bizcalc-user-guide-B1.4.pdf with a page footer "GB-BizCalc User Guide · B1.4 · Page N of M" rendered on every page post-walk. (2) Every section heading inside the User Guide (all 16 of the numbered h2s from "1. The GripBlades™ Inventor Is Awesome" down to "16. Where Your Data Lives & Cross-Browser Saving") now has a small orange "PDF" button pinned to the far right of the heading row, built with a new .ug-pdf-btn-sm class (same orange-pill colors, compact 2px/9px padding, 10px font). To make that layout work without reflowing prose, the global .how-body h2 selector was flipped to display:flex with justify-content:space-between and the heading text was wrapped in a new <span class="ug-h2-label"> with flex:1/min-width:0 so long titles wrap gracefully while the PDF pill stays right-aligned. The Table of Contents — which also uses h2/h3 nodes — was intentionally NOT given PDF buttons; only the actual content sections have them. Clicking a section PDF button calls ugExportSectionPDF(anchorId), which reads the element by its existing anchor id (how-inventor, how-roi-interface, how-overview, how-inputs, how-calc, how-tariffs, how-live, how-offline, how-news, how-hts, how-sources, how-exports, how-limits, how-exposure, how-apikeys, how-saving) and walks forward through nextElementSibling until the next h2 or the .how-back-top link, rendering only that section's content into a PDF saved as gb-bizcalc-user-guide-{anchorId}-B1.4.pdf. (3) The CHANGELOG heading itself — top-left of the changelog pane inside the User Guide — now has a third PDF pill to its immediate right (new .ug-pdf-btn-cl class, ~16px left margin for breathing room). ugExportChangelogPDF() iterates every .changelog-row inside #changelogScroll, renders each one as a 12pt bold colored version line ("B1.4") with the date beside it at 9pt gray, then the note body at 9pt in #3c3c3c with a thin gray divider between entries. Output saved as gb-bizcalc-changelog-B1.4.pdf. The button's onclick uses event.stopPropagation() so clicking it does NOT trigger the new full-page open flow described below. (4) Clicking anywhere on the Changelog box — but NOT on the PDF pill or the search widget described in #5 — now opens a full-page Changelog overlay (ugMaybeOpenChangelogFull → ugOpenChangelogFull) that covers the User Guide at z-index 2147483100 (one tier above the modal itself, which sits at 2147483000). The overlay uses .ug-cl-full-bg / .ug-cl-full and contains a header ("Changelog — Full History" + round "✕" close button) plus a scrollable .ug-cl-full-body into which we copy the inner HTML of #changelogScroll. Closing via the X button, clicking the backdrop outside the panel, or calling ugCloseChangelogFull() simply strips the .on class and restores body overflow. The whole-box cursor is set to pointer via .ug-cl-clickable so users get a visible affordance, and the button / input defense-in-depth in ugMaybeOpenChangelogFull checks event.target.closest('button, input, .ug-cl-search') before opening to guarantee nested interactive widgets never trigger it. (5) A new CMD+F-style search widget (.ug-cl-search) now sits centered between the PDF pill and the italic "Scroll to see full history ↓" readout at the top-right of the changelog header. The input auto-detects version-number queries (regex /^[A-Za-z]\d+\.\d+$/ — matches B1.2, A8.8, C10.0, etc.) and, if matched, locates the exact .changelog-ver span and scrolls that row into view inside #changelogScroll with a single pulsing active marker. Anything else is treated as a free-text phrase — ugChangelogSearch walks every text node inside #changelogScroll and wraps every case-insensitive occurrence in a <span class="ug-cl-mark">, tracking each span in the _ugClMatches array. The counter cell between the input and the arrows shows "N/M" (e.g. "3/17") or an em-dash when empty; the ◀ / ▶ arrow buttons call ugChangelogSearchStep(-1 / +1), updating which mark carries the .active class (orange #ffb648 + outline) and scrolling it into view. Enter advances to next, Shift+Enter goes back, Escape clears. Arrows disable themselves when there are zero hits. Both the input and the arrow buttons stopPropagation so clicks never trigger the full-page open from #4. ugCloseChangelogFull does not clear the search input so users can reopen the popup with their prior query intact. Version bumped to B1.4 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B1.4.pdf, mfr-cost-analysis-B1.4.pdf, roi-…-B1.4.pdf, cost-per-click-history-B1.4.pdf, trade-terms-chart-B1.4.pdf, gb-bizcalc-user-guide-B1.4.pdf, gb-bizcalc-user-guide-{section}-B1.4.pdf, gb-bizcalc-changelog-B1.4.pdf — and the saved HTML filename "GB-BizCalc - B1.4 (with-data).html"). 85 revisions tracked.
B1.3Apr 2026Three User-Guide-focused polish fixes on top of B1.2. (1) The "The GripBlades™ Inventor Is Awesome" section was relocated from the bottom of the User Guide (#15, its original position from B0.0 #14) to the TOP as section #1 — it's the first thing visitors now see when they open the guide. All downstream sections shift by +2 to accommodate the inventor move plus the new ROI-Calc Interface Guide described below: Overview & Purpose 1 → 3, Inputs 2 → 4, Tariff Calculation Engine 3 → 5, Each Tariff Explained 4 → 6, Live Rate Lookup 5 → 7, Offline Mode & Caching 6 → 8, News Feed 7 → 9, HTS Code Lookup Tools 8 → 10, Source Transparency 9 → 11, PDF & Excel Export 10 → 12, Limitations & Disclaimer 11 → 13, Internet Exposure & Privacy 12 → 14, API Keys 13 → 15, Where Your Data Lives & Cross-Browser Saving 14 → 16. All anchor ids (how-inventor, how-overview, how-inputs, how-calc, how-tariffs, how-live, how-offline, how-news, how-hts, how-sources, how-exports, how-limits, how-exposure, how-apikeys, how-saving) remain unchanged so any existing deep-link to a section still resolves. The old bottom-of-guide location has a pointer comment left in place for future maintainers. (2) A brand-new section — "2. ROI-Calc Interface Guide" (anchor id="how-roi-interface") — sits between the Inventor section and Overview & Purpose, giving users a top-to-bottom plain-English walkthrough of how ROI-Calc's four-level hierarchy (MACRO COST BASIS → CUSTOMER CARD → PROFILE → PACK PANEL) interacts. Covers: the drag-and-drop reorder mechanics for MACRO COST BASIS slots and for CUSTOMER CARDS; the drag-over-to-copy mechanics for all three levels (MACRO COST BASIS swap — B1.1 #4, CUSTOMER CARD swap — B1.2 #1, PROFILE swap — pre-existing) with the shared "CONFIRM DATA SWAP FOR {name}" popup pattern and its new "X" close button from B1.2 #4; the DUPLICATE CARD right-click option introduced in B1.1 #5; the LANDED $/PC propagation chain from the MACRO COST BASIS profile down into every pack panel's four readout cells (LANDED / PIECE = $0.3807, LANDED / 10 PACK = $3.8070, LANDED / 10 PK MC x120 = $456.84, LANDED / PALLET = $20,557.80 style) with the multiplier logic at each tier; how that propagated landed cost feeds into the A-DIST, B-DIST, C-DIST, D-DIST, E-DIST and A-MSRP, B-MSRP, C-MSRP, D-MSRP, E-MSRP rows to compute GripBlades $ Profit and ROI % in real time; the right-click context menu on Profile buttons with the B1.2 #3 horizontal color bar on top and the CLEAR PROFILE / RESET COLOR / Say Hi to Julie action buttons below; and the per-panel 2-row notepad tucked under the RESET button in every pack panel. The section closes with a single-paragraph mental-model summary: "MACRO COST BASIS → CUSTOMER CARD → PROFILE → PACK PANEL. Every level has drag-to-reorder, drag-over-to-copy, and right-click shortcuts. Change LANDED $/PC once and the propagation runs top-to-bottom without you touching anything else." (3) The two note-entry rows that live between the RESET button and the pack-size title inside each pack panel (10 PACK / 40 PACK MC / etc.) now show a GRAYED-OUT "Notes:" placeholder at the far left of the top row when the field is empty. The placeholder is rendered via a pure-CSS .roi-notepad-area:empty::before pseudo-element (italic #9aa0a6 "Notes:" with pointer-events:none so clicks fall through to the editable layer), so it's cosmetic only — you can't select it, cursor past it, delete it, or type between its characters. The moment the user focuses the field and starts typing, the contenteditable node is no longer :empty and the placeholder disappears on its own; typed text starts at the far-left of row 1 just like before. If the user backspaces or clears all their content, the placeholder reappears automatically. Two JS shims (in roiNoteSave after the row-cap trim, and in roiHydrateNotes after the initial text fill) explicitly reset el.innerHTML to '' when the text content is empty — this is required because browsers routinely leave a stray <br> inside a contenteditable when you clear it via Backspace or Cut, and an element containing a <br> no longer matches CSS :empty, which would suppress the placeholder. The 2-row cap from B0.3 #2, the per-row underline guideline background, the RESET-button clearance (right:95px), the paste-as-plain-text path (roiNotePaste), and the per-card × per-profile × per-basis × per-SKU persistence store are all unchanged — this fix only adds the "Notes:" hint. Version bumped to B1.3 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B1.3.pdf, mfr-cost-analysis-B1.3.pdf, roi-…-B1.3.pdf, cost-per-click-history-B1.3.pdf, trade-terms-chart-B1.3.pdf — and the saved HTML filename "GB-BizCalc - B1.3 (with-data).html"). 84 revisions tracked.
B1.2Apr 2026Five polish fixes on top of B1.1, focused on parity and consistency between the MACRO COST BASIS and CUSTOMER CARD drag-drop/right-click flows plus PDF spacing cleanup. (1) CUSTOMER CARDS now support the same DRAG-DROP DATA COPY interaction that MACRO COST BASIS profiles got in B1.1 #4. Grab any Customer Card and drag it onto another — on mouse-up, a "CONFIRM DATA SWAP FOR / {target card name}" popup appears ~8px above the cursor (identical spec to the basis-swap popup: header prompt, framed target-name cell, red CONFIRM pill). Click CONFIRM and the TARGET card receives a deep-copy (JSON round-trip) of the SOURCE card's entire roiCardData[cardId] tree — all 5 Profiles A-E × all bases × all SKUs for state / promos / notes — plus the per-Profile color assignments from roiSubColors[cardId]. The SOURCE card is never mutated; its name, lock state, data, and position in roiCustomerCards are all left alone. The old B1.1 drag-drop-reorder behavior on Customer Cards has been ENTIRELY REPLACED with this copy flow (so the left/right hairline drop indicators were swapped out for a full-slot amber halo via .roi-card-swap-target, matching the basis-swap target styling). Untitled cards show "(unnamed card)" in the confirm popup so the prompt still reads sensibly. If the target is the active card, the visible inputs refresh immediately via roiLoadActiveIntoVisible + loop over roiCurrentSkus calling roiRecalc(i). Dismiss via X button (new — see #4), click-outside (capture-phase document listener), or Esc. (2) The "Delete Entire Profile" row was REMOVED from the right-click menu on individual Profile buttons (the A-E row inside each Customer Card). That action — which used the word "Profile" but actually deleted the whole parent Customer Card — was redundant with the Customer Card's own "DELETE CUST. CARD" right-click option and invited misfires from users who thought it meant the profile. The dead 'delete' / 'confirmDelete' branches inside roiSubProfileCtxAction were also stripped. The remaining actions (CLEAR PROFILE, RESET COLOR, Say Hi to Julie) moved up one slot in the visual stack as a result. (3) The color-swatch bar inside the Profile right-click menu is now HORIZONTAL and sits on TOP of the action buttons, instead of being a vertical column on the left side of them. Code rename: the .roi-sub-ctx-colorbar CSS class was renamed to .roi-sub-ctx-horizontalcolorbar so the orientation is explicit in the source too, and the surrounding flex container was flipped from flex-direction:row (two-column) to flex-direction:column (stacked). A bottom border on the color row separates it from CLEAR PROFILE / RESET COLOR / Say Hi to Julie underneath. Single-click a color to apply it to the right-clicked Profile (same behavior as A9.8, just repositioned). (4) The MACRO COST BASIS "CONFIRM DATA SWAP FOR" popup (B1.1 #4) and the new Customer Card swap popup (B1.2 #1) both now have a small "×" close button in their top-right corners. Same .rbsc-close-btn class drives both — 18×18 transparent button that sits position:absolute inside the popup with a light-gray hover background. Gives users a visible, click-targetable cancel affordance in addition to the existing click-outside / Esc dismiss paths. The popups' top padding was bumped from 12px to 22px so the X doesn't overlap the "CONFIRM DATA SWAP FOR" heading text on narrow viewports. Clicking the X routes to roiHideBasisSwapConfirm() / roiHideCardSwapConfirm() respectively — zero data mutation. (5) Spacing around the black 1pt horizontal rule under pack subheadings in the ROI-Calc PDFs was rebalanced. The rule previously sat at 4pt below the subheading baseline and 8pt above the LANDED COST READOUT row, so it felt visually glued to the subheading and left an awkward gap to LANDED COST READOUT. B1.2 #5 bumps both gaps to ~10pt, putting the rule roughly halfway between the subheading and the LANDED COST READOUT. The ensure(N) call that reserves vertical space before emitting the subheading group also grew from 28 → 34 to accommodate the extra whitespace. Applied identically in both exportRoiBasisPDF (per-basis export) and exportRoiPDF (per-pack export) so per-basis and per-pack PDFs stay visually consistent. Version bumped to B1.2 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B1.2.pdf, mfr-cost-analysis-B1.2.pdf, roi-…-B1.2.pdf, cost-per-click-history-B1.2.pdf, trade-terms-chart-B1.2.pdf — and the saved HTML filename "GB-BizCalc - B1.2 (with-data).html"). 83 revisions tracked.
B1.1Apr 2026Seven polish fixes on top of B1.0, all seeded from an uploaded B1.0 HTML that preserved the user's live ROI-Calc data (Customer Cards, MACRO COST BASIS profiles, all per-basis state/promos/notes were copied through intact — only the version metadata was bumped). (1) The DISTRIBUTOR COST price table ("Tier Price A-DIST $5.49 B-DIST $5.99 C-DIST $6.49 D-DIST $6.99") has been REMOVED from the DIST. DATA PDF variant (exportRoiBasisPDF when isDist === true). Rationale: the same A-DIST / B-DIST / C-DIST / D-DIST price points already appear inline in the "A-MSRP · ref $9.99 vs A-DIST ($5.49) $4.50 45.05%" ROI line directly below, so the separate table was redundant and took up a full quarter-page. The FULL DATA PDF variant (isDist === false) still shows the table as before, unchanged. The distLetters const that feeds the table is declared OUTSIDE the gated block so the DIST. ROI FROM RETAIL PRICING section immediately below can still reference it to render its per-tier ROI rows in both variants. (2) The two right-hand columns of each DIST. ROI FROM RETAIL PRICING row — the dollar-profit figure (e.g. "$4.50") and the percentage figure (e.g. "45.05%") — now have proper column headers ("GripBlades $ Profit" and "ROI %") rendered in bold 9pt uppercase gray directly above the first tier row. Previously readers had to infer what each column represented from context; now the semantic is explicit. A 235-gray 1px rule sits under the headers to visually detach them from the data rows. Header block is emitted at the TOP of each DIST. ROI section (once per pack size in exportRoiPDF, once per pack × basis in exportRoiBasisPDF) so it always precedes its data. (3) Every pack subheading in the ROI-Calc PDFs — e.g. "10 PACK — SKU/MPN 10010 · UPC 785366284171" and "40 PACK — SKU/MPN 40040 · UPC 785366284188" — now has a solid 1pt BLACK horizontal rule spanning full page-width minus margins rendered underneath it, with ~4pt of whitespace between the subheading baseline and the rule plus ~8pt of clear space before the next section begins. jsPDF setDrawColor(0,0,0) + setLineWidth(1) + doc.line() emit the rule; line-width is restored to 0.1pt afterward so subsequent strokes (the 235-gray divider rules) aren't accidentally promoted to 1pt black. Applied identically in both exportRoiBasisPDF (per-basis export) and exportRoiPDF (per-pack export). (4) The MACRO COST BASIS profile buttons now support DRAG-DROP DATA COPY. Grab any basis button and drag it onto another basis button — on mouse-up, a new CONFIRM popup appears ~8px above the cursor with "CONFIRM DATA SWAP FOR" on row 1 and the full target-basis name on row 2 (in a framed surface2 cell that breaks long names). Click CONFIRM (red pill, uppercase) and ALL data from the source basis is deep-copied into the target basis: the global roiState[basis] and roiPromos[basis] buckets, the per-landed + per-logged roiBasisLanded and roiBasisLog buckets, and the nested roiCardData[cardId][subId].{state,promos,notes}[basis] for every Customer Card × Profile in the app. The source basis is LEFT UNTOUCHED — this is a copy, not a swap, exactly as the user specified ("The 1st MACRO COST BASIS button profile you are dragging and dropping will have no changes done to it"). A full UI refresh fires afterward (roiLoadActiveIntoVisible + roiLoadInputsFromState + roiSyncBasisLandedInputs + roiSyncBasisLoggedDisplays + a loop over roiCurrentSkus calling roiRecalc(i)) so numbers redraw instantly. The popup is dismissible via outside-click (capture-phase document listener) or Escape, and uses the same viewport-edge flip-below + horizontal clamp math as the Customer Card delete popup from B1.0 #3. The previous basis-reorder behavior that lived on the same drag-start / drop handlers in B1.0 has been entirely REPLACED — drag-drop on the basis buttons now only triggers this data-copy confirm flow. A new amber 2px outline + 4px box-shadow halo (.roi-basis-swap-target CSS class) highlights the hovered target during the drag so users can see exactly which basis is about to receive the data. (5) Right-clicking any Customer Card now surfaces a TWO-ROW context menu: top row "DELETE CUST. CARD" (red, unchanged behavior) and a NEW second row "DUPLICATE CARD". Clicking DUPLICATE fires immediately (no confirm step — duplication is non-destructive) and creates a full deep-copy of the source card inserted at srcIdx + 1, pushing every card to the right of the source one slot further right. Copied data includes the complete roiCardData[cardId] tree (all 5 Profiles A-E × all 3+ bases × all SKUs × state/promos/notes) via JSON round-trip, plus per-Profile color assignments from roiSubColors[cardId]. Lock state is NOT copied (clone starts unlocked). Auto-naming uses a " N" suffix convention: "MARK" → "MARK 1", and if MARK 1 already exists the next free integer is used — so MARK → MARK 2 when MARK 1 is present, and duplicating MARK 1 when MARK 2 exists correctly yields MARK 3 (the algorithm strips a trailing " <integer>" from the source name to compute the base, then scans all existing card names case-insensitively to find the smallest free N). Names respect the 17-char cap from B1.0 #6. Unnamed source cards ("") propagate as empty name so the user can type one in. After render, the new card scrolls into view with behavior:'smooth'. (6) All ROI-Calc user data from the uploaded B1.0 (Customer Cards, MACRO COST BASIS profiles, SKU-level inputs, per-basis landed costs, promo tiers, notes) is preserved in B1.1 unchanged — only version metadata was bumped. Achieved by seeding B1.1 as a byte-for-byte copy of the uploaded B1.0 HTML rather than regenerating from the previous B1.0 template, which would have blown away the embedded localStorage-free data attributes. (7) The "(See Chart Here)" link in Tariff-Calc no longer has a text-decoration underline — it stays blue (var(--accent)) + cursor:pointer so it still reads as clickable, but the visual underline is removed for a cleaner inline treatment next to the Trade Terms / FOB dropdown row. The old text-underline-offset:2px style is removed alongside so no stray offset rule lingers. Version bumped to B1.1 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B1.1.pdf, mfr-cost-analysis-B1.1.pdf, roi-…-B1.1.pdf, cost-per-click-history-B1.1.pdf, trade-terms-chart-B1.1.pdf — and the saved HTML filename "GB-BizCalc - B1.1 (with-data).html"). 82 revisions tracked.
B1.0Apr 2026Seven polish fixes on top of B0.9. (1) The GB-Calc popup window now remembers both its SIZE and its POSITION across open/close cycles, with two distinct persistence scopes. FIRST open after loading the HTML: the popup always spawns CENTERED on the main browser window (computed from screenX + screenY + outerWidth + outerHeight at open-time). When the user drags it anywhere on the OS desktop — including off the browser window to the side — that moved position is captured into an in-memory _gbCalcLastPos and re-used if the popup is closed and re-opened in the SAME page session. If the user closes the main HTML file (or refreshes), the position memory is cleared so the popup re-centers on next open, matching the user's "center again on full reload" spec. SIZE is persistent across EVERYTHING: the popup is resizable, and on every close + beforeunload the current outerWidth/outerHeight are written to a new localStorage key gbBizCalcCalculatorWin_v1 (clamped to min 260×460). Subsequent opens — in the same session OR after a hard reload — resume at that last-saved size. A 400ms interval watcher polls the open popup's screenX/Y + outerWidth/Height so mid-session drags and resizes are captured continuously without waiting for a close event. (2) The TWO pack-section headings ("10 PACK ROI — 10 blades/pack · 1,200 blades/MC · 54,000 blades/pallet" and "40 PACK ROI — 40 blades/pack · 960 blades/MC · 51,840 blades/pallet") inside the MACRO COST BASIS PDF export now render as ORANGE FILLED BARS (jsPDF setFillColor 244/131/79 + rect 'F' mode, full page-width minus the left/right margin, 26pt tall, 8pt bottom gap) with BOLD WHITE 12pt text — visually matching the Download PDF pill buttons used elsewhere in the HTML. Before B1.0 the pack headings were plain 13pt dark-gray text with a 1px underline, which blended into the body copy and made long multi-page PDFs harder to scan; readers now instantly see where each pack section starts. The orange bar is emitted for BOTH pack sizes identically so 10-Pack and 40-Pack blocks read as parallel sections. (3) The right-click context menu on a Customer Card — previously labeled "DELETE" and positioned UNDER the cursor — has been rebuilt to match the MACRO COST BASIS profile-delete UX from B0.9 #3. STEP 1 ("DELETE CUST. CARD", renamed from the shorter "DELETE") now opens CENTERED on the cursor (cursor sits at the geometric center of the button, both horizontally and vertically, via anchorX - rect.width/2 + anchorY - rect.height/2), not below it — so the user can click-confirm without moving the mouse, but can't trigger it by reflexive fast clicks since the menu physically replaces the ground the cursor was hovering over. STEP 2 ("CONFIRM DELETE", renamed from "Click to Confirm Deletion") opens ABOVE the cursor with an 8px gap, forcing a deliberate upward move to commit the destructive action. Both steps keep the same viewport-edge clamping + flip-to-opposite-side fallback math used by the basis ctx menu from B0.9 #3, so they can't escape the visible viewport even when the click happens at the page edges. (4) Right-clicking any of the MACRO COST BASIS profile buttons previously surfaced a "DELETE THIS PROFILE" option (B0.7 #4 rename); this has been tightened to "DELETE PROFILE" for visual consistency with the shorter "DELETE CUST. CARD" label on the Customer Cards ctx menu introduced in fix #3 above. The confirm-step wording "CONFIRM DELETE" (from B0.7) is unchanged. (5) The MACRO COST BASIS name fields are now HARD-CAPPED at 4 rows × 7 characters = 28 chars total. A new onbeforeinput handler (roiBeforeBasisNameInput) blocks the 29th keystroke and any Enter-driven line insertions BEFORE they hit the DOM, so the cursor physically cannot advance past the last cell of row 4. A paired onpaste handler (roiOnBasisNamePaste) force-converts clipboard content to plain text, strips CR/LF, and splices only the remaining room into the caret via document.execCommand('insertText'). roiOnBasisNameInput is also strengthened: when raw text arrives already over 28 (IME compose / drop / programmatic edit), it truncates the DOM node itself and restores the caret to the end via window.getSelection + Range so the 4×7 visual budget is never exceeded from any input path. (6) Customer Card name inputs accept ONE MORE character — cap bumped from 16 → 17 on both the input element's maxlength attribute, the placeholder text "Customer name (max 17)", the live oninput slice (.slice(0,17)), AND the CUSTOMER-search box's own maxlength so users can still search for names up to the new 17-char ceiling. (7) The WHITE BACKGROUND underneath the ROI-Calc tab content was restored. The B0.7 #1 change had stripped `background:#fff` from #panelRoi's inline style to fix a 1px gray anti-alias seam caused by the panel's own border-radius colliding with the shell's border — but on tall content the transparent panel was revealing the #f2f2f7 body color below the shell's own `min-height:80vh`, giving ROI-Calc a visibly different bottom tail than Tariff-Calc / MFR-CostCalc. B1.0 re-adds ONLY `background:#fff` (explicitly NOT the `border-radius:0 0 8px 8px` that was the root cause of the anti-alias seam; the shell's own radius already defines the rounded corners). The MACRO COST BASIS PDF popup fix from B0.8 #3 (position:fixed + appended to document.body, escaping all ancestor overflow contexts) is fully preserved — it was never dependent on the panel's background, so this change leaves the popup's stacking untouched. Version bumped to B1.0 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B1.0.pdf, mfr-cost-analysis-B1.0.pdf, roi-…-B1.0.pdf, cost-per-click-history-B1.0.pdf, trade-terms-chart-B1.0.pdf — and the saved HTML filename "GB-BizCalc - B1.0 (with-data).html"). New localStorage key: gbBizCalcCalculatorWin_v1 (persistent GB-Calc popup window size). 81 revisions tracked.
B0.9Apr 2026Five polish fixes on top of B0.8. (1) GB-CALC is now a true OS-level popup window (window.open()) instead of an in-page modal, so the calculator can be dragged anywhere on the user's screen — including out of the browser window entirely, off to the side on the Mac desktop — and stays open while the user works in the main GB-BizCalc tabs. The popup has its own red "×" close button in the top-right corner; closing the popup leaves the main app untouched, and closing the main HTML (or the main tab) auto-closes the popup via a beforeunload handler. All calculator state (display + tokens + accumulator + pending op) still persists through localStorage between sessions. (2) The calculator now shows a running-expression line above the big readout, mirroring the macOS Sequoia/Sonoma Calculator: typing "24×52−3.5" prints "24 × 52 − 3.5" in a small gray line above the display as the user enters each operand, so it's easy to catch a mistyped number before hitting "=". The expression is kept in a tokens[] array (e.g. ['24','×','52','−','3.5']) and updates live after every digit + operator + equals press; AC wipes it. (3) The DELETE-profile right-click popup (step 1 — "DELETE THIS PROFILE") now renders so the cursor sits at the vertical AND horizontal CENTER of the button, not below it (previous attempt placed it below, which the user clarified as incorrect). The confirm popup (step 2) still sits above the cursor as before. Both positions use getBoundingClientRect() + viewport-edge clamping so the menu can't escape the visible area. (4) Custom MACRO COST BASIS profile names now wrap at 7 characters per row, max 4 rows, matching the permanent "(FEU) 40 FT. Cost Basis" layout. CSS: width:7ch + overflow-wrap:anywhere + word-break:break-word + max-height:60px overflow:hidden. The text-input character cap is lowered from 32 to 28 (7 × 4) so overflow can't happen past the 4th line. (5) The MACRO COST BASIS PDF export (exportRoiBasisPDF) now skips any pack block — and any individual SKU panel — that has NO user-entered distributor / MSRP data in it, so the PDF no longer wastes space on "10 PACK ROI — 10 blades/pack · ..." headings with nothing underneath them. Per-SKU panels with no inputs are also skipped, since their landed-cost readouts just repeat info already shown in the top MACRO COST BASIS summary.
B0.8Apr 2026Four fixes on top of B0.7. (1) A new "GB-CALC" button is added to the LEFT of the LEADS button in the top-right toolbar cluster, styled identically to LEADS and BUILD NOTES (26px height, same font, white bg + black border resting / black bg + white text on hover, with a small calculator-keypad SVG icon on its left). Clicking it opens a stock macOS-Calculator-style popup (dark #1c1c1e shell, 60px rounded-pill keys, orange #ff9f0a operator keys with the macOS-convention white-bg/orange-text "active" highlight on the pending operator, light-gray #a5a5a5 top-row utility keys, dark-gray #505050 digit keys, zero-key spans two columns) with AC, +/-, %, digit, decimal, and +−×÷= functionality plus keyboard input bindings (0-9, . , + - * / = Enter % Esc Backspace). The FULL calculator state (current display, pending operator, accumulator, justDidEq flag, running expression line) is persisted to a new localStorage key gbBizCalcCalculator_v1 on every keypress, and rehydrated every time the popup re-opens — so if the user closes the calculator, navigates to other tabs, and comes back later, the numbers they were typing AND the last output they got are still there. The Save Data / Load Data JSON buttons in the middle column re-center automatically via the existing flex:1 + justify-content:center middle-column layout, which rebalances when the right-side toolbar cluster widens to accommodate GB-CALC. (2) New MACRO COST BASIS entries now show "---" as a CSS-only grayed-out background placeholder (text3 color at 0.45 opacity via a ::before pseudo-element on a data-placeholder attribute) instead of a literal "_____" placeholder string inside the contenteditable span. Because the placeholder is a pseudo-element and not real text, the user can type directly over it without having to select-and-delete first; the placeholder vanishes the moment the first character is typed, and reappears if the field is emptied again. Built-in bases (Pallet / TEU / FEU) always show their stock names and never get the placeholder. (3) The MACRO COST BASIS per-button PDF popup (FULL DATA / DIST. DATA) — which the user has reported as broken in five consecutive build cycles (B0.3 / B0.5 / B0.6 / B0.7, plus the shadow-without-popup regression in B0.7) — was finally traced to a CSS-side root cause: .roi-basis-scroll sets overflow-y:hidden (needed for the horizontal macro-basis scroll UX), and every prior strategy placed the popup as a descendant of that scroll container. The popup's body rendered ABOVE the scroll container's visible region and got clipped, while its box-shadow falloff leaked DOWNWARD into the visible area — which is exactly what the user was reporting. B0.8 escapes the overflow-clipping subtree entirely: .roi-basis-pdf-menu is now position:fixed (viewport-coordinate, unaffected by any overflow ancestor), and showRoiBasisPdfMenu() creates a fresh popup and appends it as a DIRECT CHILD of document.body. Placement runs synchronously — btn.getBoundingClientRect() at click time + popup.getBoundingClientRect() after append + viewport-edge clamping (min/max 4px margin on all sides) — with no requestAnimationFrame, no transitions, no off-screen parking. If there isn't room above the button the popup flips below; if the popup would clip the left or right edge it slides inward. z-index stays at 2147483000 so no modal/overlay can cover it. (4) The right-click DELETE flow on custom MACRO COST BASIS options had its two popups REPOSITIONED so they never visually occupy the same on-screen area. Step 1 ("DELETE THIS PROFILE") now appears BELOW the cursor with an 8px gap (reverting B0.7's above-cursor placement of step 1) — the natural spot for a "just right-clicked, here's the menu" response. Step 2 ("CONFIRM DELETE") continues to appear ABOVE the cursor with the same 8px gap. Because both popups re-use the same #roiBasisCtx element, "DELETE THIS PROFILE" is fully gone the moment "CONFIRM DELETE" renders, so the user never sees both labels at once and the physical mouse-travel-path between the two steps (down then up past the cursor) makes accidental single-flick confirmation much harder. Both steps keep their horizontal centering + edge-of-viewport fallback math (flip to the other side if there's no vertical room). Version bumped to B0.8 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B0.9.pdf, mfr-cost-analysis-B0.9.pdf, roi-…-B0.9.pdf, cost-per-click-history-B0.9.pdf, trade-terms-chart-B0.9.pdf — and the saved HTML filename "GB-BizCalc - B0.9 (with-data).html"). New localStorage key: gbBizCalcCalculator_v1 (persistent GB-Calc state). 79 revisions tracked.
B0.7Apr 2026Five targeted fixes on top of B0.6. (1) The ROI-Calc tab, when active, no longer shows a thin gray dividing line beneath it — #panelRoi's inline style was carrying a leftover `background:#fff;border-radius:0 0 8px 8px` from the B0.0 #15 white-underlay work, which (despite being the same color as the shell's background) created a subtle ~1px anti-alias artifact at the panel's top edge where the shell's border and the panel's own box met. Removing the explicit background/radius makes #panelRoi match #panelTariff and #panelMfr (`padding:0.75rem 0 0;background:transparent`) so all three tabs now merge seamlessly with the panel-shell when active. (2) The tab label "TariffCalc" is now "Tariff-Calc" to follow the same hyphenated convention as "MFR-CostCalc" and "ROI-Calc". (3) The orange Download PDF pill inside the TariffCalc "(See Chart Here)" popup moved from top-right to bottom-left of the popup image, and its font color flipped from near-black (#1a1a1a) to pure white (#ffffff) so the label reads clearly against the orange #ff9f0a background. Popup padding was rebalanced from `36px 8px 8px 8px` to `8px 8px 36px 8px` to give the repositioned pill its dedicated vertical breathing room at the bottom. (4) The right-click Delete flow on custom MACRO COST BASIS options became a more explicit 2-step confirm. Step 1's button label changed from "Delete" to "DELETE THIS PROFILE", and the menu now opens ABOVE the cursor (with an 8px gap) — matching step 2's existing above-cursor geometry — so the user has to move the mouse up slightly to click (harder to trigger accidentally). Step 2's label tightened from "Click to Confirm Delete" to "CONFIRM DELETE". Both steps share the same flip-below-on-tight-viewport fallback math. (5) The MACRO COST BASIS PDF-button popup (FULL DATA / DIST. DATA), which the user has repeatedly reported as non-appearing despite reliability fixes in B0.3 #1, B0.5 #9 and B0.6 #2, has been rebuilt with a fundamentally different strategy. Prior versions used a single shared `position:fixed` singleton positioned via `requestAnimationFrame` JS coordinate math — fragile under any number of race conditions. The B0.7 implementation creates a FRESH popup element in the DOM on every click, appended as a DIRECT CHILD of the clicked `<label class="roi-basis-opt">`, and uses pure CSS (`position:absolute; bottom:100%; right:0`) to anchor it above the PDF button with ZERO JS coordinate math. The parent label is given `position:relative` on the fly (its prior position value is stashed on a data attribute and restored on hide). Outside-click dismissal moved to the capture phase with a guard that ignores clicks on any `.roi-basis-pdf-btn`, so the very click that opens the popup can't instantly close it. Version bumped to B0.7 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B0.7.pdf, mfr-cost-analysis-B0.7.pdf, roi-…-B0.7.pdf, cost-per-click-history-B0.7.pdf, trade-terms-chart-B0.7.pdf — and the saved HTML filename "GB-BizCalc - B0.7 (with-data).html"). 78 revisions tracked.
B0.6Apr 2026Two polish fixes on top of B0.5. (1) The Download-PDF button for the TariffCalc "(See Chart Here)" hover is now ORANGE (brand color #ff9f0a, not the prior dark gray) and lives INSIDE the Trade Terms chart popup as a position:absolute overlay in the popup's top-right, with 36px of dedicated top padding so it never overlaps the chart image. Crucially, the previous layout SHIFT BUG is fully eliminated: before B0.6, the pill was a real flex child of #seeChartLinkWrap (display:inline-flex + gap:4px); toggling the pill's display from none → inline-flex on hover widened the wrap, which the parent row's justify-content:space-between translated into "SEA FREIGHT METHOD — (See Chart Here)" sliding LEFT and the FOB dropdown + CALCULATE button sliding DOWN. The wrap is now plain inline-block with no flex or gap — zero width change on hover, so nothing around the link moves. The popup itself (position:fixed) plus onmouseenter/onmouseleave handlers keep the image+button alive while the mouse is inside either one, and a shared 300ms timer handles graceful fade-out. (2) The MACRO COST BASIS PDF-button popup (FULL DATA / DIST. DATA) reliability work from B0.5 #9 is retained and verified: FULL DATA produces the identical per-basis report as prior versions (GripBlades margins + LANDED readout + MSRP ROI all intact), while DIST. DATA sanitizes for distributors by stripping the MACRO COST BASIS landed $/pc summary, the per-SKU LANDED COST READOUT block, the "GripBlades $ Profit" + "Margin %" columns from the DISTRIBUTOR COST table, and the entire "GRIPBLADES ROI FROM MSRP (DIRECT-TO-END-USER)" section — so only Distributor + MSRP headings and numbers remain, with no GripBlades data leaking. The PDF variant tag ("— DISTRIBUTOR EDITION") is printed in the PDF header banner and the filename footer so the report's scope is unambiguous. Version bumped to B0.6 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B0.6.pdf, mfr-cost-analysis-B0.6.pdf, roi-…-B0.6.pdf, cost-per-click-history-B0.6.pdf, trade-terms-chart-B0.6.pdf — and the saved HTML filename "GB-BizCalc - B0.6 (with-data).html"). 77 revisions tracked.
B0.5Apr 2026Nine targeted fixes, ending the B0.x batch. (1) TariffCalc "Itemized Tariff Breakdown" readout was overwriting itself on longer tariff names — e.g. "MFN Base Duty — Specific ($0.03/pc)BASE$0.0300/pc" and "Merchandise Processing Fee (MPF)FEE0.3464% (capped)" — because .tariff-name carried white-space:nowrap which forced long labels to overflow horizontally into the .ttag badge and the right-aligned rate column. Fixed by restructuring the row into a flexing .tariff-row with a flex:1 .tariff-name-wrap that wraps onto a second row (flex-wrap:wrap + row-gap:2px + min-width:0) when the name + tag don't fit beside the rate; .tariff-name itself switched from white-space:nowrap to white-space:normal + overflow-wrap:break-word + word-break:break-word so each word breaks at the wrap point cleanly. Line-height bumped to 1.35 so wrapped rows breathe. The downloadable PDF version of the same readout got the matching treatment: 120pt is now reserved on the right for the fmt(amt) right-aligned text, and the left-side name is run through jsPDF splitTextToSize(nameMaxW) so long names spill onto a second line within the reserved width instead of running over the amount column. The 9pt gray description line below each row (rate — short_desc) also goes through splitTextToSize(W-2*M) so long descriptions wrap cleanly within the page width. (2) The narrative line "Persistent supplier-cost reference: 6 unit rows × 3 basis columns. Each cell shows the current value, the prior value, and its Last Updated stamp." was removed from the downloadable PDFs of the "MFR COST REF. CHART ◀▶ LANDED COST REF. CHART" section — the on-screen UI still shows the description; only the PDF output drops it per user feedback that it was noise on an otherwise clean exported chart. (3a) In the same chart PDF, the "Current $/blade" column was given more horizontal separation from the left-side "Unit Row" label column: the unit-label column width was widened from 110pt → 150pt and the Current $/blade + Prior Val. columns each got 110pt of their own (up from a tighter prior spec), so the three data columns spread more comfortably across the page and don't crowd the Unit Row labels. (3b) For the TariffCalc "(See Chart Here)" link, the "Download PDF" pill is no longer positioned absolutely UNDERNEATH the link — it now sits INLINE to the RIGHT of the link text as a sibling flex element with margin-left:6px, so on hover the pill appears directly beside "(See Chart Here)" at the same baseline rather than floating below it. The pill keeps its mouseenter timeout-cancel so the cursor can glide right from the link onto the pill without triggering dismissal. (4) The destructive-action confirm button (the one shown after right-clicking a Profile and choosing CLEAR PROFILE, or Delete Entire Profile) now RENDERS ABOVE the cursor with an 8px gap instead of directly at or below the cursor — so the user has to DELIBERATELY move the mouse UPWARD to hit the CONFIRM button. This defeats the double-click footgun where an accidental rapid second click immediately after choosing the action would confirm destructive clears on a wrong Profile. The anchor coords (_roiSubCtxState.anchorX / anchorY) are now stashed from the ORIGINAL right-click event at menu-open time, and the confirm-phase re-render uses requestAnimationFrame to measure the freshly-populated menu's bounding rect so ny = anchorY - r.height - gap lands the button directly above the cursor; viewport clamping flips it below the cursor only if there's no room above, and also clamps horizontally so the menu never slides off-screen. (5) (User intentionally skipped item #5 in the request batch.) (6) MACRO COST BASIS buttons (Pallet Cost Basis / (TEU) 20 FT. Cost Basis / (FEU) 40 FT. Cost Basis / any custom bases) now stack the basis NAME on top with the PAD LOCK icon DIRECTLY UNDERNEATH it, instead of the prior horizontal layout where the padlock sat to the left of the name. A new .roi-basis-opt-namelock flex container (flex-direction:column, align-items:center, gap:4px) wraps the .roi-basis-opt-title and the .roi-basis-lock button; the title is first in DOM order so it renders on top and the padlock below it. The wrapper inherits flex:1 1 auto + min-width:0 so long names still wrap within the basis option cell. The padlock's native click behavior (toggle locked / unlocked, swap ROI_LOCK_SVG_LOCKED ↔ ROI_LOCK_SVG_OPEN, enable contenteditable on the title when unlocked, focus + selectAll the title for immediate typing) is unchanged — only the visual position moved. (7) First-run default: the MACRO COST BASIS row now seeds with ONE EXTRA empty renameable basis entry (the 4th button on the right of the three built-ins — Pallet / TEU / FEU) so the default layout shows 4 buttons out of the box. The seeding is gated by a new localStorage key gbBizCalcRoiBasisSeeded4th_v1 so it only fires ONCE per install — if the user later deletes that 4th basis, it will NOT respawn on reload; if the user wants more than 4, they click the "+" button to add additional renameable bases. Clicking the padlock icon UNDER the basis name toggles rename-mode: unlocked → the name becomes contenteditable and gets auto-focused with its existing text pre-selected so the user can type over it; click the padlock again to lock the name back in place. All of this reuses the existing roiToggleBasisLock + roiOnBasisNameInput wiring from B0.3 #3 — the B0.5 change is purely the seeding + vertical stacking. (8) The 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panels (.roi-grid) are now positioned CLOSER to the MACRO COST BASIS frame above them — the grid's top padding dropped from 0.75rem to 0, so the only vertical gap between the bottom of .roi-controls-panel and the top of the first SKU panel is now the controls panel's own 1rem bottom margin. That matches exactly the gap between the CUSTOMER CARDS frame and the MACRO COST BASIS frame (controlled by .roi-controls-panel margin-top:1rem), so the three stacked frames visually read as an evenly-spaced column rather than the prior tighter-top-looser-bottom layout. (9) The per-basis PDF popup menu (FULL DATA / DIST. DATA) that opens from each MACRO COST BASIS "PDF" button now RELIABLY appears. The prior implementation measured the menu's bounding rect SYNCHRONOUSLY on the same tick as the display:none → display:block transition, which returned 0-sized rects on some paint schedules and parked the popup off-screen so the user never saw it. The rewrite captures the PDF button's anchor rect FIRST (before innerHTML repaint, since evt.currentTarget goes null on the next tick), parks the menu at -9999px / -9999px to avoid a pre-position flash, adds the .open class, and then positions it inside a requestAnimationFrame() callback once the browser has fully laid out the menu — so the actual measured height drives the above/below-button anchor math correctly. The menu's z-index was hoisted from 1500 to the max-safe integer region (2147483000) so it renders on top of any modal / news popup / overlay; visibility:hidden + opacity:0 in the default state plus the matching visibility:visible + opacity:1 in .open guarantee the menu can't be silently suppressed by any inherited visibility state on an ancestor. Both menu items (Full Data / Dist. Data) now also stopPropagation on their own onclick so the outside-click dismisser doesn't race with the export call. The DIST. DATA variant continues to strip exactly as before: the MACRO COST BASIS landed-$/pc summary is replaced with a simple "REPORT SCOPE" stub, the per-SKU LANDED COST READOUT block is dropped, the DISTRIBUTOR COST table loses its "GripBlades $ Profit" + "Margin %" columns, and the entire "GRIPBLADES ROI FROM MSRP (DIRECT-TO-END-USER)" section is relabeled "MSRP — RETAIL PRICING" with the GripBlades Margin + ROI % columns removed. The "DIST. ROI FROM RETAIL PRICING · A-MSRP $9.99" sections, all MSRP prices, and all promo % values stay intact in the DIST variant so the distributor still sees the pricing and ROI-from-retail math they need. The FULL DATA variant is unchanged — everything the PDF used to contain before B0.3 #1 is still there. Version bumped to B0.5 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B0.5.pdf, mfr-cost-analysis-B0.5.pdf, roi-…-B0.5.pdf, cost-per-click-history-B0.5.pdf, trade-terms-chart-B0.5.pdf — and the saved HTML filename "GB-BizCalc - B0.5 (with-data).html"). New localStorage key: gbBizCalcRoiBasisSeeded4th_v1 (one-time flag tracking whether the default 4th basis has been seeded for this install). 76 revisions tracked.
B0.4Apr 2026Three fixes, one of which (A) resolves a critical regression from B0.3 that cascaded into six user-visible symptoms all at once. (A) CRITICAL — the B0.3 build introduced a Temporal Dead Zone (TDZ) ReferenceError in the <script> top-level: roiEnsureBasisBuckets() was being called at module-init time via a ROI_BASES.forEach(b => roiEnsureBasisBuckets(b)) line placed BEFORE the const roiCardData = roiCardDataLoad() declaration further down the file, yet the helper's body touched roiCardData directly via Object.keys(roiCardData || {}). Because const bindings throw on access during their TDZ regardless of any "|| {}" fallback (the OR evaluates AFTER the identifier access, which is exactly what triggers the throw), the <script> threw a ReferenceError at its top level during page load, which HALTED the rest of the script's execution — meaning every downstream function definition, const declaration, event-listener registration, and init call after that line simply never ran. That is why B0.3 silently broke SIX unrelated pieces of UI at once: the "MFR COST REF. CHART ◀▶ LANDED COST REF. CHART" chart disappeared from MFR-CostCalc, the 10 Pack / 40 Pack size selector buttons inside MFR-CostCalc stopped activating and the "MFR Pack Cost — Select Pack Size First" heading froze, the "Completed Blade Build" and "Blade Component Entry" buttons inside MFR-CostCalc went dead, the saved "Ace Hardware HQ" Customer Card (and all of its prior-session memory) stopped appearing in the Customer Cards bar, the "+" icon for MACRO COST BASIS existed but did nothing when clicked, and the new B0.3 #1 PDF popup menu couldn't even be evaluated because the button handlers never got attached. The fix REMOVES that top-level ROI_BASES.forEach call entirely (replaced in place with an explanatory comment marking it as deferred), and DEFERS the same per-basis bucket initialization to inside roiBuild() — which is itself only called after roiCardData has been declared and loaded, so there's no more TDZ window. Inside roiBuild() the new try { ROI_BASES.forEach(b => roiEnsureBasisBuckets(b)); } catch(_){} block plus a trailing roiRenderBasisOptions() call now seed every basis's nested per-card / per-profile buckets at build time with full access to a real roiCardData object. Net effect: the script executes cleanly end-to-end, and all six B0.3 regressions lift together — the dual chart + switcher arrows return, Pack Size 10/40 selection + its "MFR Pack Cost" heading update return, both "Completed Blade Build" + "Blade Component Entry" buttons resume working, the Ace Hardware HQ Customer Card rehydrates from localStorage with all its prior Profile data intact (several additional empty customer cards render to the right of it by default, per existing A9.3+ default-card behavior), the MACRO COST BASIS "+" button's add-flow creates new basis entries correctly, and the B0.3 PDF 2-selection popup menu (FULL DATA / DIST. DATA) is now reachable + functional again. (B) TariffCalc "(See Chart Here)" link gains a HOVER-TRIGGERED "Download PDF" pill replacing the B0.2 right-click context menu that the user couldn't consistently invoke. The pill is rendered as an absolutely-positioned sibling span inside a new #seeChartLinkWrap next to the "(See Chart Here)" text — mouseover the link text and both the chart popup image (existing A9.8 behavior) AND the new dark-gray "⤓ Download PDF" pill appear together underneath it. The pill has its own onmouseenter handler that cancels the shared dismiss timer (keepSeeChartPdfPill = clearTimeout(tradeChartTimeout)), so the user can glide the cursor DOWN from the link to the pill without triggering the 300ms hide; clicking the pill calls the existing downloadSeeChartPDF() routine from B0.2 (orientation auto-selects landscape vs. portrait from the image's natural dimensions; the image is letterbox-fit inside a 24pt margin via jsPDF). mouseleave on either the link OR the pill starts the shared dismissal timeout that hides BOTH elements. The right-click context menu is kept as a fallback path so power users who prefer it aren't disrupted. (C) MACRO COST BASIS selector gets DRAG-AND-DROP REORDER matching the Customer Cards drag-drop pattern. New CSS rules .roi-basis-opt.dragging (opacity .45, scale .98) + .roi-basis-opt.drag-over-left + .roi-basis-opt.drag-over-right (box-shadow edge-hairline cue in var(--accent)) provide the visual drop-zone feedback; new handlers roiBasisDragStart / roiBasisDragOver / roiBasisDragLeave / roiBasisDrop / roiBasisDragEnd manage the drag lifecycle (dragstart blocks initiation when the drag originates from a typed-over input / contenteditable label / padlock button so the label's own edit flow isn't interfered with). roiRenderBasisOptions injects the dragAttrs string (draggable="true" + all 5 on-event handlers bound to the basis id) into every basis label wrapper at render time. On successful drop, ROI_BASES is mutated in place via splice to the new index order and then persisted under a new localStorage key gbBizCalcRoiBasisOrder_v1 via roiBasisOrderSave(); on page load an IIFE (applySavedBasisOrder) reads the saved id-array from the new key, intersects it with the current ROI_BASES set (in case a previously-custom basis was deleted), and reorders ROI_BASES in place before any basis UI renders — so the user's hand-arranged basis order survives page reloads identically to the Customer Cards order under gbBizCalcRoiCards_v1. Deleting a custom basis and adding a new one both call roiBasisOrderSave() to keep the persisted order in sync. Existing built-in Pallet / (TEU) 20 FT. / (FEU) 40 FT. entries remain undeletable (matches B0.3 #3) but are fully reorderable alongside any custom basis entries. Version bumped to B0.4 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B0.4.pdf, mfr-cost-analysis-B0.4.pdf, roi-…-B0.4.pdf, cost-per-click-history-B0.4.pdf, trade-terms-chart-B0.4.pdf — and the saved HTML filename "GB-BizCalc - B0.4 (with-data).html"). New localStorage key: gbBizCalcRoiBasisOrder_v1. 75 revisions tracked.
B0.3Apr 2026Three ROI-Calc refinements focused on PDF-output granularity, notepad sizing, and the Macro Cost Basis selector. (1) Each of the three small "PDF" buttons under PALLET / (TEU) 20 FT. / (FEU) 40 FT. Cost Basis now opens a TWO-ROW popup menu (FULL DATA / DIST. DATA) instead of immediately downloading a file. "FULL DATA" produces the complete per-basis ROI export exactly as before. "DIST. DATA" produces a DISTRIBUTOR EDITION variant that (a) strips every GRIPBLADES-MARGIN column/row out of the per-pack, per-SKU breakdown tables, (b) strips the entire "LANDED $/pc" readout block at the top (it reveals our internal landed cost to whoever receives the PDF), and (c) tags the header banner with "— DISTRIBUTOR EDITION", the footer with " · DIST", and appends "-dist" to the filename so the sanitized copy self-identifies if re-shared. The popup menu dismisses on outside click / Escape, and the two rows share the same button-like styling so the two variants are discoverable side-by-side rather than hidden behind a right-click. (2) The ROI-Calc PANEL NOTEPAD (redesigned to 4 rows in B0.2) has been CAPPED AT 2 ROWS. The faint horizontal guideline rules now render as only 2 underline guides (background-size Y cut in half), and roiNoteCapToMax() trims any typed or pasted input that would push scrollHeight past the 2-row ceiling. This was driven by a visual collision on narrower viewports: the 4-row notepad was stretching down past the floating RESET button on the right of the panel head and visually punching through into the SKU-title stack underneath. The 2-row cap restores clean vertical separation and keeps the notepad strictly within the header band — all existing 3-line legacy notes still read correctly (roiNotesMigrate still joins the legacy array into the new free-text shape; any excess beyond 2 rendered rows is preserved in storage but scrollable within the visible area). (3) The MACRO COST BASIS selector is now FULLY DYNAMIC — it emulates the Customer Cards "+" add-flow. The heading and its hint ("pick scale of ordering…") have been nudged slightly to the right, and a small "+" icon button now sits to the LEFT of the heading. Clicking "+" appends a new renameable cost-basis option to the RIGHT of the existing ones, inside a horizontally-scrolling container (the row scrolls left / right off the page exactly like the Customer Cards bar, and auto-scrolls to reveal the new entry on creation). New custom bases initialize with a "_____" placeholder name + empty LAST VALUE LOGGED and LANDED $/PC fields; right-click a custom basis to get a Delete item that transitions to a Confirm popup positioned ABOVE the cursor with an 8px gap (flipping below if there's no room above). The three ORIGINAL bases (Pallet / (TEU) 20 FT. / (FEU) 40 FT.) are permanent and undeletable by design. The native radio oval has been replaced on all options (including the originals) with a small PAD LOCK SVG icon — click it to toggle rename-mode on the basis label. While unlocked the label becomes contenteditable; while locked it's inert. Lock state is persisted per-basis alongside the basis name in a new localStorage key (gbBizCalcRoiBasisMeta_v1), so custom labels + lock state survive reloads. All four per-basis data buckets (roiState / roiPromos / roiBasisLanded / roiBasisLog) plus nested per-Card / per-Profile entries (roiCardData[cardId][sid].state|promos|notes) seed fresh on creation and are scrubbed on deletion, so switching to a freshly-added basis on any card immediately works with no undefined-bucket crashes. PDF exports (ROI per-basis + Cost History) now resolve custom bases through roiBasisDisplayName() so headers, footers, and per-basis summaries print the user-entered label instead of the internal "CB1"/"CB2" id. Save Data / Load Data JSON export + import + the Save Into File bake/hydrate flow pick up the new meta key automatically via the existing gbBizCalc* key sweep — no new code in gbCollectAllAppKeys. Version bumped to B0.3 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B0.3.pdf, mfr-cost-analysis-B0.3.pdf, roi-…-B0.3.pdf, cost-per-click-history-B0.3.pdf, trade-terms-chart-B0.3.pdf — and the saved HTML filename "GB-BizCalc - B0.3 (with-data).html"). 74 revisions tracked.
B0.2Apr 2026Five UX fixes across the ROI-Calc panel notepads, the TariffCalc Trade Terms chart, the Macro Cost Basis selector row, and the top header data-button cluster. (1) ROI-Calc PANEL NOTEPAD REDESIGN — the three separate contenteditable lines (B0.0/B0.1) have been replaced with ONE contenteditable area that auto-wraps typed text into up to 4 rows. Four faint horizontal guideline rules are drawn UNDER each typing row by a repeating CSS linear-gradient on the area's background (1px stripe of var(--border) at the bottom of every 22px tile), so the user sees 4 evenly-spaced underline guides regardless of how many rows are currently filled. JS roiNoteCapToMax() trims any input (typed OR pasted) that would push the rendered scrollHeight past the 4-row cap (line-height 22px × 4 + padding). The notepad's RIGHT edge has also been pulled ~30px further LEFT (right:32px instead of B0.1's right:0) so it stops ~6 character widths short of the floating RESET button on the right of the panel head. Storage shape changed from notes[basis][mpn] = [l1,l2,l3] → notes[basis][mpn] = 'free-text' but reads are backward-compatible with the legacy 3-element array (joined with spaces on load), so no data is lost from B0.0/B0.1 profiles. Paste is intercepted (roiNotePaste) to insert PLAIN TEXT only — pasted markup / newlines are stripped. (2) TariffCalc "(See Chart Here)" link now has a RIGHT-CLICK CONTEXT MENU with a single "⤓ Download PDF" item that exports the TRADE TERMS chart popup image as a letter-sized PDF via jsPDF. Orientation auto-selects landscape vs. portrait from the image's natural dimensions; the image is letterbox-fit inside a 24pt margin. Works fully offline because the chart is inlined as a base64 data URL on a hidden <img> inside #tradeChartPopup. Menu dismisses on outside click or ESC. (3)+(4) MACRO COST BASIS panel headings (PALLET COST BASIS / (TEU) 20 FT. COST BASIS / (FEU) 40 FT. COST BASIS) can now VERTICALLY STACK when the horizontal band is too narrow to fit the full title on one row. Text wraps at word boundaries with text-align:center + line-height:1.25 so "PALLET COST" sits on row 1 with "BASIS" centered under it on row 2. The prior white-space:nowrap + overflow:hidden + text-overflow:ellipsis treatment (which silently clipped the title's tail under the Last Value Logged block when the viewport narrowed) is dropped, so long titles are fully readable at narrow widths. All three basis panels (PALLET / TEU / FEU) benefit. (5) Removed the "Save Into File ⤓.html" button from the top-header centered data-button cluster — it was not working reliably for users across browsers. The Save Data ⤓.json + Load Data ⤒.json pair remains and re-centers automatically because the container's justify-content:center + flex:1 layout self-distributes the two surviving buttons across the middle slack. Version bumped to B0.2 everywhere (title bar, User Guide header + modal, version badge, all PDF headers, all PDF footers, all PDF filenames — tariff-analysis-B0.2.pdf, mfr-cost-analysis-B0.2.pdf, roi-…-B0.2.pdf — plus trade-terms-chart-B0.2.pdf, and the saved HTML filename "GB-BizCalc - B0.2 (with-data).html"). No new localStorage keys; no schema migrations beyond the backward-compatible note-shape read path. 73 revisions tracked.
B0.1Apr 2026Three small UX polishes for the ROI-Calc Customer Cards bar and ROI panels. (1) The four CLIPBOARD slots now render in a soft LIGHT GRAY (#d1d5db bg, #9ca3af border, #4b5563 text) when EMPTY — the prior bright red (#d6322a) was visually loud and read as an "error" / "alert" state when in practice an empty CLIPBOARD is a perfectly normal idle state. The bright green ".has-data" filled state is unchanged so a clipboard that's actually holding a captured Profile snapshot still pops visually against the new neutral gray empties — the active vs. idle distinction is now much clearer at a glance. (2) The 3-line per-panel notepad inside every "10 Pack" / "10 Pack MC" / "40 Pack" / "40 Pack MC" SKU card has been MOVED + NARROWED so it fits into the previously-empty horizontal band BETWEEN the SKU title block on the left (the bold "10 Pack" / "10 Pack MC" / etc. with the SKU/MPN + UPC lines under it) and the floating RESET button on the right. The notepad is now absolutely-positioned inside .roi-card-head (top:0; left:175px; right:0 — the parent's existing padding-right:58px reserves the RESET button area). The first notepad line ends up sitting on the SAME ROW as the SKU title and the RESET button, with lines 2 + 3 cascading down from there at the original line spacing (margin-bottom:7px between lines, line 3 sharing the head's bottom border as its underline). The .roi-card-head min-height was set to 100px so the head reserves enough vertical room for the absolutely-positioned notepad, ensuring the panel below doesn't shift up when the head's title block is shorter than the 3-line notepad. Net effect: ~50px of vertical real estate reclaimed per ROI panel without losing the notepad, and the notes now read as a clear "annotations" column to the right of the SKU title rather than a separate stack below it. (3) NEW hover-popup pill ABOVE each "A-MSRP" / "B-MSRP" / "C-MSRP" value input field in the GRIPBLADES ROI FROM MSRP (DIRECT-TO-END-USER) section. The narrow input width was clipping the trailing cents on longer dollar amounts (e.g. "$1,050.99" rendered as "$1,050.9" with the final 9 cut off), so on mouseover OR keyboard focus a small dark gray pill (#1f2937 bg, white mono text, ~13px) now floats 6px above the input showing the FULL formatted dollar amount with thousands commas + 2 decimal places (e.g., "$1,050.99"). The pill is implemented as a sibling div inside .roi-input-wrap; pure CSS controls visibility (opacity:0 → 1, visibility:hidden → visible on .roi-input-wrap:hover OR .roi-input:focus ~ .roi-msrp-tip) and the new roiUpdateMsrpTip() helper updates the pill text on every keystroke (oninput), every hover-in (onmouseenter), and every focus event. Sub-profile hydration paths (roiLoadInputsFromState, roiLoadActiveIntoVisible) call the new roiHydrateMsrpTips() so freshly-loaded MSRP values are reflected in their tips without waiting for the user to hover. The popup pill carries a small 5px-wide downward triangle pointer pseudo-element (::after) so it visually "anchors" to the input below it. Distributor cost inputs (1.A-DIST through 4.D-DIST) are unaffected — only the MSRP value inputs got the hover popup since those are the ones the user identified as visually clipped. Version bumped to B0.1 everywhere (title bar, User Guide header + modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis-B0.1.pdf, mfr-cost-analysis-B0.1.pdf, cost-reference-charts-mfr-and-landed-B0.1.pdf, roi-…-B0.1.pdf — plus cost-per-click-history-B0.1.pdf, and the saved HTML filename "GB-BizCalc - B0.1 (with-data).html"). No new localStorage keys; no schema migrations. 72 revisions tracked.
B0.0Apr 2026Sixteen targeted improvements; first "B" generation release per the version-reset rule (A9.9 → B0.0, never A9.10). (1) NEW 3rd "DIST. ROI FROM RETAIL PRICING · C-MSRP" panel added to the right column of every "10 Pack" / "10 Pack MC" / "40 Pack" / "40 Pack MC" SKU card, paralleling the existing A-MSRP and B-MSRP retail-ROI matrix blocks. The retail-ROI loop now iterates ['A','B','C'] instead of ['A','B'], and the per-basis ROI PDF exporter automatically picks up C-MSRP rows because msrpLetters already filtered through ['A','B','C']. (2) The active "MFR" / "LANDED" chart heading + its triangle arrow now render in their tab's brand color when active — MFR in dark green (#2d6a4f), LANDED in GripBlades dark blue (#003e8a). Inactive headings stay faded gray. (3) The TariffCalc tab-folder logo icon (small folded-corner glyph next to "TariffCalc" in the tab strip) was recolored from amber to GripBlades dark blue (#003e8a) so the tab brand-color matches the LANDED chart accent. (4) The MFR chart heading + arrow tint matches #2 — both arrow + title share the same active/inactive coloring rule. (5) The Pallet / TEU / FEU column-head backgrounds inside the active chart now color-match the chart (green for MFR, dark blue for LANDED) with white heading text, instead of the previous neutral gray. The corner cell above the row labels also picks up the chart color. (6) Those Pallet/TEU/FEU column headings render at 13.5px (was ~11.5px) — two font sizes larger to make the basis labels readable at a glance from across the room. (7) The 6 row labels on the left side of every chart (10 PK PC, 10 PACK, 10 PK MC, 40 PK PC, 40 PACK, 40 PK MC) now wrap onto THREE centered lines per cell — the leading number ("10" or "40") on line 1 (14px bold), the pack-vs-MC token ("PK BX" / "PK MC") on line 2 (10.5px), and the trailing descriptor on line 3 — so labels read top-to-bottom instead of running off the right side of narrow row-head cells. (8) Version reminder formalized — the per-batch version bump rule is "no double digits": A9.9 must roll to B0.0, never A9.10. Same rule applies inside any letter generation (B9.9 → C0.0, never B9.10). All version strings, PDF banners, file titles, and the User Guide modal have been updated end-to-end to read B0.0. (9) CRITICAL FIX — "Save Into File ⤓.html" now correctly preserves AND restores ROI-Calc data, AND the restored copy's ROI-Calc buttons remain fully clickable. Two root causes were diagnosed: (a) the JSON payload baked into the script tag was NOT script-tag-safe — any user data containing the substrings "</script>", "<!--", "-->", or unicode line/paragraph separators (U+2028/2029) would terminate the inline script tag prematurely and break every JS handler defined after the bake block, which is why "many ROI-Calc buttons don't work or aren't clickable" after restore. The new gbEscapeJsonForScriptTag() helper escapes those four sequences before injection. (b) The hydration block at the top of the inline script wrote the baked values to localStorage but did NOT force a reload, so module-level constants like roiCardData = roiCardDataLoad(), roiCustomerCards, and the clipboard array (all initialized once during script evaluation) held the empty defaults captured BEFORE hydration completed. Now hydration logs a per-key + ROI-key count to the console and triggers a one-time setTimeout(reload) — the hydrationMarker prevents an infinite loop. The result: all gbBizCalc* keys (Customer Cards, Profile data, MSRP/Distributor inputs, promos, basis-landed values, basis logs, panel notes, MFR/LANDED chart values, Build Notes, Leads notes, vibe image, sub-profile colors, cost-per-click history, AND the tc_* tariff cache) survive the round trip, and the alert at the end now reports the count of restored keys for transparency. (10) NEW Download PDF button inside the "Cost Per Click History" popup (the modal opened from the Cost Per Click readout). Clicking it generates a multi-page PDF organized by year → month → day → time → cost — each year gets a section heading, each month a sub-heading, each day a row showing per-click time-of-day and per-click dollar cost, plus aggregate stats (total clicks, total spend, average cost) at the top. Filename is cost-per-click-history-B0.0.pdf. (11) TariffNews refresh now supports up to 10 articles (was 5) and the popup grid scrolls vertically (max-height 680px, scrollbar-gutter stable) when more than fits in one viewport. The refresh error path now distinguishes between a generic API failure and a missing API key — when no Anthropic API key has been entered, the inline feedback reads "✗ Add your Anthropic API key to enable refresh" instead of the previous misleading "✗ Refresh failed — using cache". The cache-only fallback still works without a key. (12) Chart-arrow rearrangement — the chart switcher header now reads heading-arrow-arrow-heading (MFR title · ◀ · ▶ · LANDED title) instead of the prior arrow-heading-spacer-heading-arrow layout, putting both triangle arrows in the visual center where they belong. The PRIOR VAL. cell on every MFR/LANDED chart row also picked up a right-click context menu — right-click any PRIOR VAL. value, then click the menu's "CLEAR" option to confirm and zero just that one cell's prior value (matches the existing two-step CLIPBOARD clear menu pattern; auto-dismisses on Escape, outside click, or window scroll). (13) NEW horizontal swipe-scroll between MFR and LANDED charts — touchstart/touchend with a 50px horizontal threshold + 60px vertical tolerance + 800ms timeout swap charts on tablets, and a wheel-event handler with deltaX > deltaY + 18px threshold + 600ms debounce gives trackpad users two-finger horizontal swipe between charts. Swipe LEFT shows LANDED, swipe RIGHT shows MFR. The arrow click + heading click flows from #12 still work in parallel. (14) NEW User Guide section #15 ("The GripBlades™ Inventor Is Awesome") added at the bottom of the User Guide modal + TOC. Three paragraphs of light-hearted PG-13 humor praising the inventor in third person — no actual personal information, just vibes. Same length and styling as section #9 ("Source Transparency"). (15) White background restored under all four ROI-Calc panels — A9.9 #4 had layered a light-gray (--bg) underlay behind the entire ROI-Calc frame to match TariffCalc/MFR-CostCalc visual depth, but the gray bled through the per-panel cards and made the ROI-Calc tab feel busier than its sibling tabs. The underlay color reverts to #fff on #panelRoi only; TariffCalc and MFR-CostCalc keep their own underlays unchanged. (16) The MSRP numerical input blocks (5.A-MSRP / 6.B-MSRP / 7.C-MSRP) on every ROI panel were widened — the per-cell horizontal gap dropped from 24px to 10px, internal padding tightened from 6px to 2px, the input/wrap stretch to 100% width, and the "ADD PROMO" button now stacks BELOW the GripBlades Margin readout column (column-flex inside .roi-calc-lines-row) instead of sitting beside it. Previously the ADD PROMO button physically overlapped the dollar margin text + ROI percentage readout in narrow viewports, hiding key numbers. The PDF download button on the chart-switcher header now ALWAYS emits a 2-page PDF (MFR Cost Reference Chart on page 1, LANDED Cost Reference Chart on page 2) regardless of which chart is currently visible on screen — filename cost-reference-charts-mfr-and-landed-B0.0.pdf — so the export captures both reference charts in a single download every time. The legacy view-state branch in exportActiveChartPDF() was removed in favor of an unconditional dual-page render through the same _renderCostRefChartIntoPDF helper. New localStorage keys this release: none added (all 16 features reuse existing key trees); existing keys gbBizCalcRoiCardData_v1, gbBizCalcMfrCostRef_v1, gbBizCalcLandedCostRef_v1, gbBizCalcMfrChartView_v1, gbBizCalcRoiClipboards_v1, gbBizCalcVibeImage_v1, gbBizCalcRoiSubColors_v1, gbBizCalcUpdateTariffsClickLog_v1, etc. all carry forward unchanged. Version bumped to B0.0 everywhere (title bar, User Guide header + modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis-B0.0.pdf, mfr-cost-analysis-B0.0.pdf, cost-reference-charts-mfr-and-landed-B0.0.pdf, roi-…-B0.0.pdf — plus cost-per-click-history-B0.0.pdf, and the saved HTML filename "GB-BizCalc - B0.0 (with-data).html"). 71 revisions tracked.
A9.9Apr 2026Seven targeted ROI-Calc / MFR-CostCalc cleanups. (1) The 8.D-MSRP block (added in A9.7 #8) has been REMOVED from every "10 Pack" / "10 Pack MC" / "40 Pack" / "40 Pack MC" panel — feedback was that 4 side-by-side MSRP blocks made the row too cramped to scan, and the D-MSRP slot was rarely used in practice. The remaining three blocks (5.A · 6.B · 7.C) now sit in a 3-column equal-width grid (was 4-column), so each gets ~33% more horizontal real estate without changing overall card height. The msrpD field stays in the underlying data model and any value previously typed in A9.7-A9.8 is preserved in localStorage (untouched by this change) so a future restore of the 4th block would show the old data again — only the DOM nodes are gone. The roiRecalc loop also picked up a defensive null guard so the dropped DOM cells can't throw when iterating the {A,B,C,D} letter set. (2) NEW 3-line per-panel notepad inside every ROI panel head. Two new contenteditable lines were added ABOVE the existing single line (which now becomes line 3 at the bottom) — all three lines sit between the SKU title/MPN/UPC text and the dashed body separator, padded by exactly the RESET button width on both sides so they don't slide under the floating reset control. Font matches the TariffCalc body font (DM Sans, 12px, weight 400, line-height 1.5). Line 3 reuses the existing .roi-card-head bottom border as its underline so there's no double rule. The two new lines (1 + 2) are equidistant between the top frame line of the panel and the existing line 3, so the notepad reads as three evenly-spaced ruled rows. Every keystroke autosaves per (Customer Card × Profile × Macro Basis × MPN × line number) under a new sp.notes[basis][mpn] sub-tree on the active sub-profile snapshot — so each (card, profile, basis, panel) combination has its OWN independent set of three notepad lines, and switching between them swaps the visible notes in real time alongside the rest of the panel data. The card RESET button also wipes that panel's three notes for the current basis/MPN. New helpers roiEnsureNotes / roiNoteGet / roiNoteSave / roiHydrateNotes / roiClearNotesForCard wired into roiLoadActiveIntoVisible (hydrate after each profile/basis switch) and roiResetSku (clear on reset). Newlines pasted into a line are automatically squashed to spaces so the line height stays at one row. (3) The "Active: {Card} · Profile {X}" readout was MOVED from the right side of the Customer Cards bar to the LEFT side — it now sits between the "+" Add Card icon (far left) and the leftmost CLIPBOARD slot, perfectly centered in that horizontal gap via a new 4-column grid layout (auto / 1fr / auto / 1fr). The text color matches the active Customer Card's color (defaults to dark blue #003e8a; if the active Profile has been recolored via the right-click color picker introduced in A9.7 #2, the readout picks up that custom color too). An empty spacer occupies the symmetric column on the right of the CLIPBOARDs so the clipboard cluster stays visually centered. (4) Added a light-gray (--bg) underlay panel BEHIND the entire ROI-Calc frame that matches the width AND height of the TariffCalc and MFR-CostCalc tab panels — including the bottom rounded corners. Previously the ROI-Calc panel rendered against the page-default white, making it visually thinner than its sibling tabs even though the content occupied the same area. The underlay is a single inline background:var(--bg) + border-radius:0 0 8px 8px on #panelRoi; no JS or layout changes. (5) Verified: the GripBlades top-left logo +10% hover scale (transform:scale(1.1) on .gb-logo:hover, transform-origin top-left, .18s transition) is already in place from A9.8 #6 — no changes needed in A9.9. (6) The MFR Cost Reference Chart heading was shortened from "MFR COST REFERENCE CHART" to "MFR COST REF. CHART" so it sits comfortably alongside the new chart-switcher arrows added in #7. The shortened label is used in the on-screen heading; the PDF banner still reads the long form so exported reports stay verbose. (7) NEW chart switcher between two cost reference charts inside MFR-CostCalc. The chart header now reads ◀ MFR Cost Ref. Chart ▶ LANDED Cost Ref. Chart, with a small spacer between the two headings. Clicking either of the black ◀/▶ triangle arrows OR clicking a faded heading swaps which chart is visible. The active chart's heading is rendered dark; the inactive heading fades to ~42% opacity. The LANDED chart is structurally identical to the MFR chart (3 column heads, 6 unit rows, per-cell Prior Val. + input + Last Updated stamp, divider line above the 40-pack block) but starts entirely zeroed and persists to its own localStorage key gbBizCalcLandedCostRef_v1 so it doesn't collide with the MFR data. The visible-chart selection itself persists under gbBizCalcMfrChartView_v1, so reopening the file lands on whichever chart was active when it was last closed. The Download PDF button at the right of the chart header now does double duty: when the MFR chart is active it emits a single-chart PDF (mfr-cost-reference-chart-A9.9.pdf, same content as before); when the LANDED chart is active it emits a SINGLE dual-page PDF (cost-reference-charts-mfr-and-landed-A9.9.pdf) with the MFR chart on page 1 and the LANDED chart on page 2 — both rendered through the same _renderCostRefChartIntoPDF helper so the formatting stays identical between pages. The legacy exportMfrCostRefPDF() name is retained as an alias so any older call paths continue to work. Version bumped to A9.9 everywhere (title, User Guide header/modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis, mfr-cost-analysis, mfr-cost-reference-chart, roi-…-A9.9.pdf — and the saved HTML filename "GB-BizCalc - A9.9.html"). New localStorage keys added: gbBizCalcLandedCostRef_v1 (LANDED chart values) and gbBizCalcMfrChartView_v1 (last-selected chart view). Per-Profile notepad data lives inside the existing gbBizCalcRoiCardData_v1 tree under sp.notes — no new top-level key. 70 revisions tracked.
A9.8Apr 2026Eight polish-and-easter-egg tweaks across the Customer Cards / header workflow. (1) Profile color picker is now SINGLE-LEFT-CLICK instead of drag-and-drop: open a Profile's right-click menu, click any color square in the vertical color bar, and that color is applied immediately to the Profile that was right-clicked. The drag-to-Profile-button workflow has been removed entirely (drop-target highlighting and the temporary draggable-color machinery were stripped out); CLIPBOARD-to-Profile and Profile-to-Profile data drags are unaffected. The squares now show a hover scale-up + ring outline + cursor:pointer to make the click affordance obvious. (2) The "Active: {Card} · Profile {X}" readout has moved DOWN from row 1 of the Customer Cards bar to row 2 — it now lives on the same row as the "+" Add Card icon and the 4 CLIPBOARD slots, right-aligned at the far right of that row, and rendered in a lighter gray (text3 / 11px / 600) to match the rest of the row. The top row is reserved for the heading and search box. (3) The 4 CLIPBOARD slots are visibly WIDER (128 → 156 px each) and the auto-rename format on data drop changed from "{Card} {Letter}" to "{Card} - {Letter}" (e.g., "Ace Hardware HQ - A"); the per-slot character cap rose from 16 to 20 to fit the new separator + a longer card name comfortably. (4) The Customer Cards panel heading was renamed from "CUSTOMER CARDS · NAME & SAVE COSTS & MARGINS" to "CUSTOMER CARDS · CALC. COSTS & MARGINS & SAVE CUST. PROFILES" — clarifies the panel's primary verb-noun pair (calculate, then save profiles) for new users. (5) The TariffCalc "(See Chart Here)" mouseover image is another 30% larger — the inline tariff-rates chart popup grew from 900 px to 1170 px max-width, so individual rate lines are easier to read at-a-glance without zooming. (6) The GripBlades logo at the top-left corner of the page now scales up to 110% on hover with a smooth .18s transition (transform-origin top-left, so it grows DOWN/RIGHT and never pushes neighbouring elements around). Cosmetic; no click action. (7) The Save Data ⤓.json / Load Data ⤒.json / Save Into File ⤓.html buttons were extracted from the LEFT title-bar row (where they sat right after the User Guide button) and moved to a new horizontally-centered cluster in the MIDDLE of the page header — the cluster is now centered between the right edge of the User Guide row and the left edge of the LEADS button, and aligned to the top of the header so it visually pairs with the User Guide button instead of the bottom-right LEADS stack. The hidden <input type="file"> moved with its sibling Load Data button so the click-to-pick wiring still resolves. (8) The "Vibe" hover hot-zone was widened to cover BOTH words "Vibe Coded" — the entire word-pair now turns red together on hover and either word triggers the saved-image popup. The wrapping <span class="vibe-word"> was simply enlarged; all click/right-click handlers (image picker, image clear, popup show/hide/follow-cursor) are unchanged, and the popup empty-state copy was updated to say "Click \'Vibe Coded\'…". Bonus easter egg: a 4th option "Say Hi to Julie" was added to the bottom of the Profile right-click menu's actions column — clicking it floats a "Hi Julie ! :)" toast (heading-sized, 22px bold red on white border) at the menu's location for exactly 1 second and then auto-dismisses. (User intentionally skipped item #9 in the request batch.)
A9.7Apr 2026Eight refinements for the Customer Cards / ROI workflow. (1) Profile buttons relabeled from numbers 1-5 to letters A-E (1=A, 2=B, 3=C, 4=D, 5=E) — purely a display change; underlying data IDs stay numeric so all existing Profile data hydrates without any migration. Every reference (button text, right-click menu, "Click to Confirm Clear Profile X" message, active-card readout, PDF page header, PDF footer) now shows the letter. (2) NEW vertical color picker on the Profile right-click menu — 5 stacked draggable color squares (mid-bright yellow, green, purple, pink at matching value/chroma, plus the BREAKING-news orange-red) sit on the LEFT side of the menu. Click and drag any square out of the menu and onto ANY Profile button (A-E, on any Customer Card) to recolor that Profile. The letter and the has-data dot stay visible on top via white-text + drop-shadow. Colors persist across reloads in localStorage (key gbBizCalcRoiSubColors_v1) and are auto-purged when their parent Customer Card is deleted. (3) NEW "RESET COLOR" option in the Profile right-click menu (below CLEAR / DELETE) — removes any user-assigned color and restores the stock blue (when the Profile has data) or stock white-gray (when empty). (4) NEW right-click context menu on all 4 CLIPBOARD slots — single CLEAR option with a "click to confirm" guard that empties the slot's data and resets its name back to "CLIPBOARD" (turning it red again). Replaces the prior browser confirm() dialog with proper menu styling matching the Profile menu. (5) Auto-rename behavior: when data is dropped onto a CLIPBOARD slot, the slot's label automatically becomes "{CardName} {ProfileLetter}" (e.g., "Ace Hardware B"), capped at 16 chars. CLEAR (right-click) reverts the label to "CLIPBOARD". (6) Customer Cards panel heading changed to "CUSTOMER CARDS · NAME & SAVE COSTS & MARGINS". (7) The 4 CLIPBOARD slots moved DOWN to a new second row inside the Customer Cards bar — same row as the "+" Add Card icon (now anchored on the far left). The clipboards are perfectly centered horizontally in the bar's full width via a 3-column grid (1fr / auto / 1fr). The active-card readout stays on the top row alongside the heading and search box. (8) Added a 4th MSRP block (8.D-MSRP) at the bottom of every "10 PACK" / "10 PACK MC" / "40 PACK" / "40 PACK MC" panel. The previous 6.B block (which sat alone in the bottom-left) moved up to share the same horizontal row as 5.A and 7.C. All four MSRP blocks (5.A · 6.B · 7.C · 8.D) now sit in a SINGLE row using a 4-column equal-width grid, shrinking the overall vertical height of every ROI card. PDFs and Profile snapshots now include the new D-MSRP value and its promo % alongside A/B/C.
A9.6Apr 2026Four targeted improvements after A9.5. (1) NEW right-click context menu on every "Profile" (1-5) button at the bottom of every Customer Card. Two options appear in the menu: "CLEAR PROFILE N" (where N is the actual number of the right-clicked Profile — 1, 2, 3, 4, or 5) wipes ONLY that one Profile's saved data (all 4 distributor prices A/B/C/D + all 3 MSRP prices A/B/C + all promo % values, across all 3 macro cost bases PALLET/TEU/FEU) and leaves the rest of the Customer Card alone — and "Delete Entire Profile" deletes the WHOLE parent Customer Card (matching the user's literal wording: it removes the card, all 5 of its Profiles, all of its data, the card name, and the lock state). Both actions go through a "Click to Confirm" two-step state machine — first click selects the action, second click confirms — so a stray right-click can't accidentally destroy data. While the confirm is pending for the "Delete Entire Profile" path, the parent card's body lights up with the existing red "delete-pending" highlight so the user can see exactly which card is about to disappear. The menu auto-dismisses on Escape, on outside click, on window scroll, and on resize, mirroring the existing A9.3 Customer Card right-click menu. New JS: roiShowSubProfileCtxMenu / roiSubProfileCtxAction / roiHideSubProfileCtxMenu / roiClearSubProfile (which deep-resets roiCardData[cardId][subId] to {state:{pallet:{},teu:{},feu:{}}, promos:{pallet:{},teu:{},feu:{}}}, calls roiCardDataSave(), and — if the cleared Profile happens to be the currently-active selection — also flushes roiState/roiPromos and re-runs roiLoadInputsFromState + roiRecalc per visible SKU so the empty state shows up immediately rather than getting silently re-clobbered by the next auto-mirror). New shared HTML node #roiSubProfileCtx reuses the .roi-profile-ctx CSS class for styling. The button template at line 5908 picked up an oncontextmenu="...roiShowSubProfileCtxMenu(event,'${c.id}','${sid}')" attribute (with event.preventDefault + stopPropagation to swallow the browser's native menu and to prevent the parent Customer Card right-click menu from also firing). (2) "Sub Profile" terminology is GONE from the user-facing UI — the buttons (1-5) at the bottom of every Customer Card are now called Profiles everywhere. The bar's heading caption changed from "Customer Cards · right-click a card to delete" to "Customer Cards · right-click a card OR a Profile (1-5) for clear/delete options". The empty-state message inside the Customer Cards bar changed from "...Each card has 5 Sub Profiles..." to "...Each card has 5 Profiles...". The disabled-input tooltip changed from "Select a Customer Card and Sub Profile (1-5) above to begin entering data" to "Select a Customer Card and Profile (1-5) above to begin entering data". The hover tooltip on every Profile (1-5) button changed from "Sub Profile {N} (...) — click to select, drag onto any Sub Profile or the CLIPBOARD to copy data" to "Profile {N} (...) — left-click to select · right-click for clear/delete · drag onto another Profile or a CLIPBOARD slot to copy". The active-card readout at the top of the bar now reads "Active: {CardName} · Profile {N}" (was "Sub {N}"). Three per-basis "PDF" button tooltips changed from "...active Customer Card × Sub Profile..." to "...active Customer Card × Profile...". The "Save Data" button tooltip changed from "...every Customer Card, Sub Profile, Build Notes..." to "...every Customer Card, Profile, Build Notes...". The User Guide section #14 ("Where Your Data Lives & Cross-Browser Saving") prose changed from "Customer Card names, Sub Profile (1–5) data..." to "Customer Card names, Profile (1–5) data...", and from "...every Customer Card, every Sub Profile, Build Notes..." to "...every Customer Card, every Profile, Build Notes...". The exported PDF page header line changed from "Customer Card: {Name} · Sub Profile {N}" to "Customer Card: {Name} · Profile {N}". JavaScript variable names (roiActiveSub, roiSubDragStart, _roiDragSubFrom, etc.) were intentionally LEFT unchanged for code stability — the rename is a UI-only change. Historical changelog rows (A9.5 and earlier) are preserved verbatim because they describe the past state of the product. (3) NEW "Vibe" hover image on the main heading. The word "Vibe" inside "GB-BizCalc - Internal Vibe Coded Biz Software" is now a clickable / hoverable hotspot. On first use, click "Vibe" once to pick an image file (PNG / JPG / GIF / WebP, ≤ ~4 MB) which is then read as a base64 data URL via FileReader and persisted into a new localStorage key gbBizCalcVibeImage_v1. From then on, hovering the cursor over the word "Vibe" pops up a small floating image panel (max 420×340 px) next to the cursor; the popup tracks cursor movement (vibeMove updates left/top per mousemove with viewport-edge clamping so the panel stays fully visible regardless of cursor position) and disappears the moment the cursor leaves the word. Click "Vibe" again at any time to replace the image; right-click "Vibe" to clear the saved image (with confirm). The word itself stays styled as ordinary heading text — only the cursor turning to pointer + a faint red text-shadow on hover hints that it's interactive. The popup also gracefully degrades when no image is saved yet: it shows a small "Click 'Vibe' to upload..." instruction inside the same popup frame on hover instead of the image. (4) NEW 4 renameable CLIPBOARD slots (replaces the single CLIPBOARD button from A9.5). Four CLIPBOARD buttons now sit in a row to the right of the Customer Card search box, each 128 px wide and able to fit a label of up to 16 characters. Each slot independently holds its own captured Sub Profile snapshot (deep-cloned via the same roiSnapshotSubProfile path) and its own user-typed name. Default label on each slot is "CLIPBOARD"; double-click a slot to swap the label span for a small inline text input (pre-filled with the current name) — type up to 16 chars and press Enter or click away to save the new label, press Escape to cancel; if you delete the name and save with empty text, the slot reverts to "CLIPBOARD" automatically. Drag-and-drop unchanged from A9.5: drag any Profile button onto any of the 4 CLIPBOARD slots to capture a snapshot into THAT slot (it turns green and gets a tooltip showing what's held); once green, drag the CLIPBOARD slot itself onto any Profile button to paste. Right-click a CLIPBOARD slot to clear (with confirm); if the slot has no held data, right-click instead resets just the typed name. The single A9.5 localStorage key gbBizCalcRoiClipboard_v1 is auto-migrated into slot 0 of the new array key gbBizCalcRoiClipboards_v1 on first hydration (then removed) so users who had a snapshot held in the A9.5 CLIPBOARD don't lose it. The drag source-flag _roiDragClipboardFrom changed from a boolean (true/false) to a slot index integer (-1 / 0..3) so paste operations know which of the 4 snapshots to use. (5) Data preservation — every existing localStorage key from A9.5 (gbBizCalcRoiCards_v1, gbBizCalcRoiCardData_v1, gbBizCalcRoiActiveCard_v1, gbBizCalcRoiActiveSub_v1, all the legacy state/promos/landed/log keys) is unchanged in A9.6, so opening A9.6 in the same browser auto-hydrates every Customer Card name + lock state, every Profile's saved Distributor / MSRP / Promo data, the active Customer Card × Profile selection, every per-basis LANDED $/pc value, and every Last-Value-Logged snapshot — the user picks up exactly where they left off in A9.5. Two new localStorage keys are added: gbBizCalcRoiClipboards_v1 (the 4 CLIPBOARD slots' persisted name + snapshot array) and gbBizCalcVibeImage_v1 (the user-uploaded "Vibe" hover image, base64 dataURL). The A9.5 CLIPBOARD key gbBizCalcRoiClipboard_v1 is migrated to slot 0 then removed. Version bumped to A9.6 everywhere (title, User Guide header/modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis, mfr-cost-analysis, mfr-cost-reference-chart, roi-…-A9.6.pdf — and the saved HTML filename "GB-BizCalc - A9.6.html"). 46 revisions tracked.
A9.5Apr 2026Four targeted improvements after A9.4. (1) CRITICAL BUG FIX — when the user clicked any "MACRO COST BASIS" option (PALLET COST BASIS / (TEU) 20 FT. COST BASIS / (FEU) 40 FT. COST BASIS), the active "LANDED $/PC" entered for that basis was NOT updating in the LANDED / PIECE / 10 PACK / 10 PK MC (×120) / PALLET readout block at the top of every "10 Pack" / "10 Pack MC" / "40 Pack" / "40 Pack MC" panel — so all of the per-piece / per-pack / per-MC / per-pallet costs being computed throughout the four panels stayed pegged to the previously-active basis (in practice, PALLET only). Root cause was a missed function call in the code path that became default after A9.3: `roiSetBasis(basis)` routes through `roiLoadActiveIntoVisible()` whenever an active Customer Card is selected (the default workflow now), and that function only restored the per-basis input/promo state from the active Sub Profile snapshot — it never re-pushed the per-basis LANDED $/pc value into the visible cells, because that value lives in a SEPARATE store (`roiBasisLanded[basis][subtab]`) that is NOT touched by the snapshot-load. The legacy non-active-card branch of `roiSetBasis` had been calling `roiUpdateLandedReadout(i)` per SKU all along; the active-card branch missed it. Fix: `roiLoadActiveIntoVisible()` now calls `roiUpdateLandedReadout(i)` for every visible SKU before recalculating, AND `roiSetBasis()` itself now ALSO runs a final defensive pass over every visible SKU at the end (regardless of which branch fired) to guarantee every panel's readout block reflects the just-selected basis. The cost basis values were correct in memory the whole time — they were just never being written to the visible DOM cells without that call. (2) Drag-and-drop SUB PROFILE data between Sub Profiles. The 5 small Sub Profile buttons (1-5) at the bottom of every Customer Card are now individually draggable. Hold the mouse down on any Sub Profile button and drag it onto ANY OTHER Sub Profile button — same Customer Card or any other Customer Card — and on release, the entire data set of the source Sub Profile (all 4 distributor prices A/B/C/D + all 3 MSRP prices A/B/C + all promo % values, ACROSS all 3 macro cost bases PALLET/TEU/FEU) is COPIED onto the target Sub Profile, overwriting whatever was there. The Sub Profile buttons themselves do NOT move or change positions; the user only sees a ghost-like dragged version of the source button during the operation, plus a yellow outline on hover-over-target. The source Sub Profile is NOT modified. If the target Sub Profile is the one currently being edited (active Customer Card × active Sub Profile), the visible inputs immediately refresh to show the newly-pasted data; otherwise the data lands in storage and the target's "has-data" indicator dot updates so the user sees the change took. Implementation: each sub-button is now `draggable="true"` with `ondragstart` setting a global `_roiDragSubFrom = {cardId, subId}`, `ondragover` previewing the target, and `ondrop` calling `roiSnapshotSubProfile(srcCardId, srcSubId)` (which deep-clones `roiCardData[src][sub]`) then `roiApplySubProfileSnapshot(tgtCardId, tgtSubId, snap)` to overwrite the target. `event.stopPropagation()` on every sub-drag handler prevents the parent Customer Card's drag-reorder behavior (added in A9.4) from firing accidentally. (3) NEW "CLIPBOARD" stage area to the right of the Customer Card search box. The CLIPBOARD button is intentionally NOT clickable; it only responds to drag-and-drop. By default it shows red ("empty"). Drag any Sub Profile button onto it and release — on mouseup the CLIPBOARD turns BRIGHT GREEN (indicating it is now holding a captured snapshot) and a fine-print readout appears immediately to its right (no block, just text) reading "{CardName} - #{SubId}", e.g. "Ace Hardware HQ - #1". The held data persists across file close/reopen via a new localStorage key `gbBizCalcRoiClipboard_v1`, so the user can capture data, close the file, and paste later. Once the CLIPBOARD is green it becomes draggable itself: click-and-hold the green CLIPBOARD graphic, drag it onto any Sub Profile button on any Customer Card, and on release that target Sub Profile's data is overwritten with the held snapshot — same paste behavior as the direct sub-to-sub drag, so the active Sub Profile (if it is the target) refreshes its visible inputs immediately. Right-click the CLIPBOARD to clear it (returns to red, no held data, draggable=false). Same `_roiDragClipboardFrom` flag in the global drag state machine routes clipboard-as-source through the same `roiApplySubProfileSnapshot()` paste path. (4) NEW 3rd MSRP block added per panel. Every "10 Pack" / "10 Pack MC" / "40 Pack" / "40 Pack MC" SKU card's MSRP section ("GRIPBLADES ROI FROM MSRP (DIRECT-TO-END-USER)") now contains 3 MSRP rows (A, B, C) instead of 2 (A, B) — laid out in a new 2-column grid: 5.A-MSRP top-left, 6.B-MSRP bottom-left, and the new 7.C-MSRP top-right. The bottom-right cell is intentionally left empty as white space + gap, exactly as requested ("a white space and gap under the 3rd one as it will be to the right by itself"). The new C-MSRP behaves IDENTICALLY to A and B: same input field shape, same "GripBlades Margin" $ + ROI % readout ("ROI if sold direct"), and an "ADD PROMO" button that drops a promo % onto the C-MSRP independently of A or B. Storage extended end-to-end: `ROI_FIELDS` now includes `'msrpC'`, `roiState[basis][mpn].msrpC` is read/written/cleared just like A and B, `roiPromos[basis][mpn].C` is read/written/cleared just like A and B, the snapshot/restore pipeline (`roiSnapshotCard`/`roiApplySnapshot`) carries `__promoC`, every `['A','B'].forEach` over MSRP letters that needed it became `['A','B','C'].forEach` (direct-margin recalc, basis-switch promo refresh, reset-card promo clear, all-promo-badge refresh, legacy promos load), and BOTH per-basis ROI PDF exporters (`exportRoiBasisPDF` and the legacy `exportRoiPDF`) now read C-MSRP values + promos and emit a third MSRP row in the GRIPBLADES ROI FROM MSRP section if any C value was entered (rows still skip if blank, per A8.8 filter behavior). Note: the right-column "Dist. ROI From Retail Pricing" matrix continues to show only A-MSRP and B-MSRP — the user only requested the 3rd MSRP heading + input + readout, not a third Retail ROI block. (5) Data preservation — all four versioned localStorage keys (`gbBizCalcRoiCards_v1`, `gbBizCalcRoiCardData_v1`, `gbBizCalcRoiActiveCard_v1`, `gbBizCalcRoiActiveSub_v1`) plus the legacy state/promos/landed/log keys are unchanged from A9.4, so opening A9.5 in the same browser auto-hydrates every Customer Card name + lock state, every Sub Profile's saved Distributor / MSRP / Promo data, the active Customer Card × Sub Profile selection, every per-basis LANDED $/pc value, and every Last-Value-Logged snapshot — the user picks up exactly where they left off in A9.4. The new `msrpC` field starts blank on existing Sub Profiles (until the user types a value), and the new CLIPBOARD starts empty (red) on first open. Version bumped to A9.5 everywhere (title, User Guide header/modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis, mfr-cost-analysis, mfr-cost-reference-chart, roi-…-A9.5.pdf — and the saved HTML filename "GB-BizCalc - A9.5.html"). New localStorage key added: `gbBizCalcRoiClipboard_v1` (the CLIPBOARD's persisted snapshot). 42 revisions tracked.
A9.4Apr 2026Seven targeted improvements after A9.3. (1) The 3 "MACRO COST BASIS" buttons (Pallet / TEU / FEU) now sit ACROSS in a single row again instead of stacked, with each per-basis "Download PDF" button shrunk to a tiny "PDF" pill so all 3 fit on one row at typical desktop widths; the panels beneath the buttons pull up correspondingly. (2) When a "10 Pack" / "10 Pack MC" / "40 Pack" / "40 Pack MC" panel is reset via its Reset button, the colored data dot on the corresponding "SUB PROFILE" button (1-5) is now also cleared so the user can SEE that the data has actually been zero'd out — internally the active Sub Profile snapshot's MPN entry for the current basis is deleted from sp.state[basis][mpn] and sp.promos[basis][mpn], roiCardDataSave() persists the change, and roiRenderCustomerCards() refreshes the bar's "has-data" indicators. (3) Customer Cards can now be DRAGGED-AND-DROPPED to reorder them. Hold the mouse down on the body of any Customer Card and drag it left or right between any other two cards; release to drop. Visual feedback shows a left/right drop-indicator on the target card. The cards remain in user-defined order from then on (the alphabetical sort that A9.3 used has been removed — new cards are added at the end and otherwise the user controls order). Interactive children (the name input, padlock, and Sub Profile 1-5 buttons) carry draggable=false + onmousedown stopPropagation so clicking them doesn't accidentally start a drag. (4) The Customer Card name search now LIVE-highlights matched cards as you type AND auto-scrolls the matched card into view inside the horizontal scroll bar (only if it's not already fully visible — no needless scroll). The search input also flashes red when nothing matches so the user gets clear feedback. (5) CRITICAL CROSS-BROWSER DATA FIX. localStorage on a file:// page is sealed off per-browser-AND-per-file-path, so opening this same .html in Chrome can't see what Safari saved. Three new buttons in the top-right corner solve this: "Save Data" downloads a JSON snapshot of all autosaved data; "Load Data" restores from that JSON in any browser (with a confirm prompt before overwriting); and "Save Into File" downloads a brand-new copy of THIS .html with all current data BAKED IN as a window.__GB_BIZCALC_BAKED_DATA = {...} block. On load, the script's hydration block at the very top copies any baked data into localStorage automatically the first time the file is opened in a new browser, so the new copy "just works" cross-browser. A new User Guide section #14 ("Where Your Data Lives & Cross-Browser Saving") explains the localStorage isolation in plain English, why data appears empty in a different browser, the recommended weekly backup workflow, and how to share data with another person via the Save Into File path. (6) The "Cost Per Click" label has been re-fixed to sit perfectly centered under the "Update Tariffs" button — the prior A9.2 attempt was still pulled left because the value's fixed-width column shifted the centered horizontal row off-center. The label now sits on its own line above the value/status line so its center is unambiguous. (7) TariffNews popup now AUTO-fetches fresh articles every time it opens (previously hit cache), and the "↻ Refresh" button gives clear inline feedback: a "⟳ Fetching latest…" pill while the API call is in flight, then "✓ X new articles loaded" when something arrived, "✓ Up to date — no new articles" when nothing was new, "✗ Refresh failed — using cache" on error, or "⚠ Offline — no fetch" when offline; success messages auto-clear after a few seconds.
A9.3Apr 2026Major ROI-Calc restructure. (1) "PROFILE LOGS" renamed to "CUSTOMER CARDS" everywhere — UI labels, internal terminology, context-menu copy, and tooltips. The heading above the bar now reads "CUSTOMER CARDS · RIGHT-CLICK A CARD TO DELETE". (2) Each Customer Card slot has been restructured. The name input + padlock now sit at the TOP of each slot (where the small "PROFILE LOG N" caption used to be — that caption has been REMOVED entirely since the user's typed name is the only meaningful identifier). The big "SAVE" button has been REMOVED — there is no longer any Save action; every keystroke auto-saves continuously into the active (Customer Card × Sub Profile). At the BOTTOM of each slot sits a row of 5 small "Sub Profile" buttons numbered 1-5, each ~half the width of the old SAVE button and the same height. The active Sub Profile button is filled in accent blue; any Sub Profile that has saved data shows a small filled dot + tinted background so the user can see at a glance which Sub Profiles within a card have data and which are still empty. (3) The "ACTIVE WORK AREA" slot has been REMOVED entirely from every card; with continuous autosave into the active Sub Profile there is no longer any need for a separate live-draft slot. The legacy roiInProg / roiSyncInProgressBtn / roiRestoreInProgress functions remain in the file as no-op safety stubs so any stale call site degrades gracefully instead of throwing. (4) The Customer Cards bar has been moved OUT of each SKU card and now sits ONCE at the very TOP of the ROI-Calc tab, ABOVE the "10 PACK ROI" / "40 PACK ROI" subtab buttons. The bar spans the full ROI-Calc frame width (formerly each SKU card had its own narrow per-card bar). The active Customer Card × Sub Profile drives the data shown in EVERY SKU card simultaneously, enforcing a single consistent customer/profile context across all 4 SKUs (10pk, 10pk MC, 40pk, 40pk MC) and all 3 macro cost bases (PALLET / TEU / FEU). (5) CRITICAL BUG FIX — deleting one Customer Card NEVER deletes any other card. Root cause of the A9.2 bug was the per-basis Profile Log layout arrays sharing the same in-memory array reference (introduced in A9.2 to keep slot identity in sync across bases); a splice on one basis therefore mutated the shared array under all 3 bases, and downstream cleanup loops then re-walked the now-shorter array and deleted entries from neighboring slot positions. A9.3 sidesteps this entire class of bug by replacing the array-of-slots model with a flat global object: roiCustomerCards[] is the ordered list of cards (each {id, name, locked}), and per-card data lives at roiCardData[cardId][subId] with each card occupying its own independent root key. roiDeleteCustomerCard(cardId) now simply splices ONE entry from roiCustomerCards and removes ONE root key from roiCardData — no shared references, no neighbor-mutation, no cascade. The "delete-nukes-all" failure mode is structurally impossible in this data model. (6) The user's existing "Ace…" Customer Cards (Ace Hardware Hartford, Ace Hardware East, Ace Hardware Downtown, etc.) auto-consolidate into a SINGLE "Ace Hardware" Customer Card on first open. A one-time idempotent migration (roiMigrateProfileLogsToCustomerCards) walks every legacy roiProfiles[basis][mpn][slotId] entry, groups by display name, treats every name starting with "Ace" (case-insensitive) as the canonical "Ace Hardware" group, and builds one Customer Card per unique non-Ace name + one consolidated "Ace Hardware" card. All migrated data lands in Sub Profile 1 of the corresponding card; Sub Profiles 2-5 start empty. The migration runs once per browser (guarded by a localStorage flag) so it doesn't keep re-folding new "Ace…" cards the user might create later. Original A9.2 localStorage keys are preserved so the user can rebuild the legacy state if needed. (7) Workflow hierarchy enforced — the user MUST select a Customer Card first, THEN pick a Sub Profile (1-5), THEN choose a Pack size (10/40), THEN pick a Basis (PALLET/TEU/FEU) BEFORE any input field accepts typing. While no card is selected, all distributor / MSRP / promo inputs are visually disabled (grayed out, cursor:not-allowed) and a "Select a Customer Card to begin" hint appears in the active-card readout at the top of the bar. Selecting a card immediately enables the inputs and loads that card's Sub Profile 1 data; selecting a different Sub Profile within the same card swaps in that Sub Profile's data while PRESERVING the user's current Pack and Basis selections (so the user's place doesn't get reset by sub-profile switches). (8) The two flanking "Download PDF" buttons (formerly one to the left of "10 PACK ROI" and one to the right of "40 PACK ROI") have been REMOVED. They are replaced by 3 NEW per-basis Download PDF buttons — one inside each Macro Cost Basis option panel (Pallet / TEU / FEU). Clicking a per-basis PDF button exports a single PDF for the active Customer Card × current Sub Profile × that basis × BOTH pack sizes (10 PACK ROI + 40 PACK ROI sections combined into one document). The PDF header banner names the customer card and sub-profile; the filename is roi-{cardName}-sub{N}-{basis}-A9.4.pdf. The new exportRoiBasisPDF(basis) function reads from the active Sub Profile snapshot directly (not from the live roiState), so the PDF reflects the active card × sub × basis even if the user has the UI pointed at a different basis at the moment of export. (9) Data model: 4 new versioned localStorage keys — gbBizCalcRoiCards_v1 (the ordered Customer Cards list), gbBizCalcRoiCardData_v1 (per-card × per-sub state+promos snapshots), gbBizCalcRoiActiveCard_v1, gbBizCalcRoiActiveSub_v1. The legacy A9.2 keys (roiProfiles, roiProfileLayout, roiActiveSlot, roiInProg) are still read by the migrator but no longer written to. roiSetBasis(basis) was rewritten to mirror the visible inputs into the active sub before swapping basis, then load that sub's snapshot for the new basis — guaranteeing zero data loss across basis switches. The keystroke handler in roiBuild() flows directly into roiMirrorVisibleToActive() + roiRenderCustomerCards() so Sub Profile button "has-data" indicators update in real time as the user types. Version bumped to A9.3 everywhere (title, User Guide header/modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis, mfr-cost-analysis, mfr-cost-reference-chart, roi-…-A9.4.pdf — and the saved HTML filename "GB-BizCalc - A9.3.html"). 40 revisions tracked.
A9.2Apr 2026Two changes. (1) ROI-Calc Profile Log clicks are no longer "sticky". Previously, when a user was on any Profile Log (including ACTIVE WORK AREA) and clicked a different Profile Log, the click was silently ignored if that target slot had no saved DATA for the currently-selected MACRO COST BASIS — the body click handler gated strictly on `entry.data` and dropped out, so the active highlight stayed on the previous slot and it felt like the click "didn't take". The handler has been rewritten to ALWAYS switch active focus to the clicked slot: if the slot has data saved for the current basis, it LOADS that data (the previous behavior), but if the slot has no data for this basis yet, the card inputs are CLEARED and the highlight moves to the clicked slot so the user gets immediate visual feedback that the click registered. Additionally, the entire Profile Log model has been refactored so slot IDENTITY (slotId, display name, lock state, and scroll position) is now SHARED across all 3 macro cost bases per-mpn — only slot DATA (distributor/MSRP/promo values) stays per-basis. Key implementation details. (a) The `roiActiveSlot` memory key dropped its basis prefix (was `${basis}|${mpn}`, now just `${mpn}`) so the active Profile Log PERSISTS as the user swaps between PALLET / TEU / FEU. (b) The per-basis layout arrays in `roiProfileLayout[basis][mpn]` are now unified to the same in-memory array reference so a push/splice on one basis propagates to all 3; `roiGetSlotOrder()` seeds/merges the union on read and writes the canonical shared array back to all bases. (c) Slot name + lock state are propagated to all 3 bases on every rename (`roiOnProfileNameInput`), every SAVE (`roiSaveProfile`), and every lock toggle (`roiToggleSlotLock`) via a new `roiSetSharedSlotMeta(mpn, slotId, meta)` helper; reads use a matching `roiGetSharedSlotMeta(mpn, slotId)` that prefers the current basis and falls back to any other basis that has the value. (d) `roiSetBasis(basis)` now honors the active Profile Log across basis switches: if the active slot is a named Profile Log, `roiApplySnapshot` is called with that slot's data for the NEW basis (or `roiClearCardInputs` is called if the slot has no data saved for this basis yet), so the user can click a Profile Log once and then tap PALLET / TEU / FEU to see that Profile Log's saved numbers for each basis cleanly. If the active slot is ACTIVE WORK AREA, the old per-basis input-state fallback is preserved (plus an added promo-badge refresh that A9.1 didn't do). (e) `roiDeleteProfileSlot` now removes the slot from all 3 bases' layouts AND all 3 bases' profile dicts; it also falls back to ACTIVE WORK AREA if the deleted slot was the active one. (f) A one-time idempotent migration function `roiMigrateSharedSlots()` runs on DOMContentLoaded: it walks every (mpn) across all 3 bases, unifies the layout into a shared array, and writes one canonical name + one canonical lock state to all 3 bases per slotId (prefers first non-empty name; lock is `true` if ANY basis had it locked). Pre-A9.2 data migrates without loss — SAVE/DELETE/rename/lock persistence is unchanged. `roiSearchProfileSlots` and `roiRenderNamedSlots` now also read names via the shared helper so alphabetization and search are consistent regardless of which basis the card is currently viewing. Two new helper functions `roiClearCardInputs(i)` (wipes distributor/MSRP inputs + promos for the current basis without touching `roiInProg` autosave) and `roiNextSlotId(basis, mpn)` (now walks all 3 bases to avoid ID collisions) round out the refactor. (2) "Cost Per Click" readout has been re-centered directly under the "Update Tariffs" button. The previous layout placed Cost Per Click on its own row right-aligned to the panel edge, which — because the label is much shorter than the row above it (API Key block + Update Tariffs button) — made it look visually offset to the left of the Update Tariffs button. The right-side header stack has been restructured: the API Key readout/entry block now sits on the LEFT of a NEW inner vertical column on the RIGHT, and that column contains the Update Tariffs button on top with the Cost Per Click label + last-cost readout in a `flex` / `justify-content:center` row beneath it. Result: Cost Per Click is horizontally centered UNDER the Update Tariffs button, as requested — the button's center and the Cost Per Click row's center now line up on the same vertical axis. No JS changes; pure HTML/CSS restructure. Version bumped to A9.2 everywhere (title, User Guide header/modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis, mfr-cost-analysis, mfr-cost-reference-chart, roi-analysis — and the saved HTML filename "GB-BizCalc - A9.2.html"). 39 revisions tracked.
A9.1Apr 2026Six changes. (1) New User Guide section "13. API Keys — Risks & Solutions" added verbatim from the prior security feedback. Covers: whether adding an API key creates a backdoor (it does not — the app runs from a local file:// origin with only outbound HTTPS to api.anthropic.com and no inbound surface), the actual risk profile (financial risk from a stolen key, not data exposure), the realistic key-leak paths (shared files with the key still typed in, unsecured machines, public GitHub pushes), GitHub bot-scraping of keys in committed files, and the protective measures (Anthropic console spend limits + alerts, dedicated throwaway keys, periodic rotation, clearing the input field before sharing the file). The section is linked from the Table of Contents as entry 13. (2) Gray gap between the three app tabs (MFR-CostCalc / TariffCalc / ROI-Calc) and their content windows has been eliminated. Root cause: the unified header's right-side stack had grown taller than the left-side logo+tabs column, and with align-items:flex-start the taller column was stretching the header and pushing the active tab's bottom edge above the panel shell's top edge. Fix has two parts. First, the unified header's align-items flipped flex-start → flex-end so the left column's tab-folder-bar (its bottom edge) sits flush against the top of the panel shell regardless of how tall the right column is. Second, the right column was restructured per the user's suggestion: the 🔑 API Key readout/entry block has been moved OUT of the cost-notice row and placed on the SAME row as the Update Tariffs button, to its LEFT. The right column is now three compact rows — [LEADS + BUILD NOTES + status pill] / [🔑 API Key + Update Tariffs] / [Cost Per Click (clickable) + last-cost readout]. Clicking the 🔑 label in-place-expands into a narrow input + Save/✕ with display:inline-flex so nothing else moves (toggleApiKeyInput rewritten accordingly). The visual result matches the A8.0 reference file: the active tab now merges cleanly into the panel shell with no visible gap. (3) "Update Tariffs" button now performs a REAL API call that updates every tariff relevant to the entered HTS code — including the inactive (struck-down / grayed-out) tariffs in the inactive_tariffs array, all their rates, all their laymens_desc / legal_detail / political_context / status_note popup text, and every short_desc row label. Implementation: fetchLiveRates() already returned both tariffs[] and inactive_tariffs[] with the full per-tariff descriptive payload; updateTariffs() now captures the response's usage object (into a new module-level lastFetchUsage) and calls renderResults() with the fresh data so the active tariff rows, the "Currently-Inactive / Struck-Down Tariffs" section, and every hover-tooltip + legal-modal popup (showTT / openLegal) re-read the freshly-returned laymens_desc / legal_detail / status_note fields and re-render automatically. Clicking Update Tariffs therefore fully refreshes the entire tariff picture for the typed HTS code — active and inactive, rates and narrative — in a single API call. (4) "$0.003–$0.015 per click" cost notice under the Update Tariffs button has been renamed to "Cost Per Click", and the actual cost of the most recent click now appears immediately to the right of those words after every API call. Cost is computed from the real response.usage object (input_tokens × $3/1M + output_tokens × $15/1M + one flat ~$0.05 web-search call) rather than a static estimate — so a click that uses more/fewer tokens shows its true dollar amount. If the response omits usage for any reason a conservative fallback estimate is used and the row is tagged "est." in the history popup. The readout hydrates on page load from the most-recent log entry so the last click's cost survives a reload. (5) "Cost Per Click" is now a clickable link that opens a full-history popup (new #costHistoryBg / #costHistoryModal). The popup shows: five summary stats (total clicks · all-time spend · this month · this year · avg/click), two bar charts side-by-side (cost per month over the last 12 months, cost per year), and a per-click log (newest first, date + time + HTS + token counts + cost). The list is vertically scrollable at max-height 360px (~10 rows per window at 36px/row) so older clicks are reached by scrolling down; the popup always opens scrolled back to the top showing the most recent clicks (scrollTop reset on every openCostHistory call). Every Update Tariffs click appends an entry {ts, costUsd, inputTok, outputTok, searches, hts, product, origin, source} to a new localStorage key gbBizCalcUpdateTariffsClickLog_v1 (capped at 5000 entries). A "Clear log" button in the footer wipes the history after a confirm. Escape key and backdrop click both close the popup. (6) Version bumped to A9.1 everywhere (title, User Guide header/modal, version badge, all four PDF headers, all four PDF footers, all four PDF filenames — tariff-analysis, mfr-cost-analysis, mfr-cost-reference-chart, roi-analysis — and the saved HTML filename "GB-BizCalc - A9.1.html"). New localStorage key added: gbBizCalcUpdateTariffsClickLog_v1. 38 revisions tracked.
A9.0Apr 2026Nine changes. (1) ROI-Calc active Profile slot now has a darker, thicker border so the user can tell at a glance which slot they're working in. A new .active-slot class adds a 2px-wide box-shadow ring (clean, no halo) plus a darker matching border-color to the currently-active slot — dark blue (#003e8a) for any named Profile Log, dark amber (#8a4a00) for the ACTIVE WORK AREA. The active-slot ring is exactly the line-weight of the Pallet Cost Basis selected button (1.5–2px solid) — heavier than the standard 1px slot border but lighter than the Profile Log Search match's 2px ring + 5px halo, so when a slot is BOTH the active slot and the search-match slot the search-match still dominates visually. Active state is per-basis × per-mpn (composite key roiActiveKey = basis|mpn) so each SKU card on each macro basis tracks its own active slot independently. The active slot is set to ACTIVE WORK AREA on every input keystroke (since the live draft has diverged from any saved profile), to a Profile Log on a successful LOAD or SAVE of that slot, and to ACTIVE WORK AREA on a roiRestoreInProgress restore. roiSetBasis, roiSyncAllProfileSlots, and roiRenderNamedSlots all re-mark the active slot after they rebuild, so basis switches and re-renders never lose the highlight. (2) The ACTIVE WORK AREA slot is now ~30% narrower (160px → 112px wide, padding 10px 6px). To accommodate the narrower frame, the autosave status note inside the slot now stacks "Autosaved" on row 1 and the timestamp (HH:MM in monospace via the new .awa-note-time class) on row 2 instead of running as one wider sentence. The note block flips to a flex column with text-align:center; the empty state ("Autosaved draft") still renders on a single row. The narrower slot also leaves more horizontal room for the named Profile Log scroll container to its right. (3) New LEADS popup added to the LEFT of BUILD NOTES on the main screen. Same notebook-paper format as BUILD NOTES (24px ruled lines via repeating-linear-gradient with background-origin:content-box / background-clip:content-box, hand-written script font, persistent localStorage textarea) but the page background is WHITE (#ffffff) instead of yellow — the page-margin red line stays for the same vertical-margin look. Clicking the LEADS button opens a centered modal with an overlay backdrop (z-index:1200, just above BUILD NOTES so a stacked open doesn't get trapped); ✕ close in the upper-right, click-outside-to-close, Escape-to-close. Textarea content auto-saves on every keystroke to a new localStorage key gbBizCalcLeadsNotes_v1; nothing migrates from BUILD NOTES so the two notepads are fully independent. The LEADS button itself uses a small white pill with a tiny person-silhouette SVG so it visually parallels BUILD NOTES (which uses a pencil glyph). Sales-leads scratchpad until the dedicated Leads feature ships. (4) Small orange "Download PDF" button added to the far right of the "MFR Cost Reference Chart" header in MFR-CostCalc. The button is sized like the SAVE buttons in the ROI-Calc Profile Logs (3px 9px padding, 9px font, font-weight:700, .05em letter-spacing) but slightly wider to fit "Download PDF" + the same file-icon SVG used on the bigger PDF buttons elsewhere. Same orange palette as the existing PDF buttons (#F4834F fill, #e0702e border, white text with text-shadow). Clicking calls a new exportMfrCostRefPDF() function that renders the entire chart (6 unit rows × 3 basis columns) as a paginated Letter-format PDF — dark header banner with timestamp + version, one sub-table per basis with columns [Unit Row · Current $/blade · Prior Val. · Last Updated], cells with no entered value render "—" for current and "-----" for prior, per-page footer "Page X of Y". Filename: mfr-cost-reference-chart-A9.0.pdf. The chart header CSS aligns the title and the new PDF button on a single row via the existing flex/justify-content:space-between (align-items changed from baseline to center for visual balance with the button height). (5) New "Prior Val." display added to the LEFT of every input cell in the MFR Cost Reference Chart. Each cell now contains a flex row [Prior Val. label + value][input], with the existing per-cell Last Updated stamp staying full-width below. The Prior Val. block shows "-----" until the cell has been overwritten at least once; once a value is changed (commit on blur with a value-change), the previous committed value moves into Prior Val. and the new value lives in the input. Storage shape upgraded from data[key] = {value, updated} to data[key] = {value, updated, priorValue}; the mfrCostRefNormCell normaliser auto-upgrades legacy entries (bare-string and A8.8 object shape) by setting priorValue:'' on read so existing supplier-cost entries are not lost. Implementation tracks a per-input baselineValue (set on focus from the saved value, comparing in formatted form so reformat-only blurs don't trigger a swap) and on blur — if the committed value differs from baseline AND baseline was non-empty — shifts baseline → priorValue. Clearing a cell that previously had a value preserves the cell entry as {value:'', priorValue:<old-value>} so the user can recover from accidental deletion by reading the prior value. Per-keystroke input handler still saves the value + timestamp without disturbing priorValue. (6) TariffNews article popup z-order fix. Clicking an article in the BREAKING dropdown was opening the article modal UNDERNEATH the news-popup-backdrop (z-index:870), so the article was unreachable. The .news-modal-bg z-index has been bumped from 400 to 920 so the article modal now sits ABOVE the news dropdown backdrop and is fully visible/clickable on open; the dropdown stays underneath until the article is closed. Clicking the article's ✕ or backdrop returns the user to the dropdown as before. (7) Version naming corrected per the no-double-digits rule. The previous draft was tagged A8.10, which violates the established versioning system (A1.1 → A1.2 → … → A1.9 → A2.0; the second segment never reaches double digits). A8.9 + new feature work therefore rolls to A9.0 — not A8.10. The entire HTML file was renamed to "GB-BizCalc - A9.0.html" and a global string replace propagated A8.10 → A9.0 across every occurrence: the <title>, the User Guide header label, the User Guide modal h2, the .version-badge, all four PDF header banners (TariffCalc / MFR-CostCalc / MFR Cost Reference Chart / ROI-Calc), every PDF footer line ("MFR-CostCalc A9.0", "ROI-Calc A9.0", etc.), and all four PDF filename strings (tariff-analysis-…-A9.0.pdf, mfr-cost-analysis-…pk-A9.0.pdf, mfr-cost-reference-chart-A9.0.pdf, roi-analysis-…-A9.0.pdf). The obsolete A8.10 file was deleted from the workspace so only the correct A9.0 build remains. (8) MFR Cost Reference Chart Prior Val. heading repositioned. The .mfr-cost-ref-prior block changed from align-items:flex-end / text-align:right to align-items:center / text-align:center, so the "Prior Val." label is now horizontally centered over its numerical readout (was right-aligned, sitting above the number's right edge). The flex column also picked up a 4px gap between label and value (line-height tightened from 1.05 → 1) so the label visually lifts up off the number with a clear breathing space, making the heading read as a proper caption above the data instead of a tightly-stacked pair. The label and value spans both got an explicit text-align:center for safety against any inherited text-align rules. No JS changes — this is purely a visual repositioning. (9) BUILD NOTES and LEADS popups are now ~35% taller, growing DOWNWARD only. The textareas inside both modals had min-height bumped 300px → 405px (1.35×), and because both modal backdrops use align-items:flex-start + padding-top:110px, the additional height extends DOWN the page from the existing top anchor — the popup's top edge stays exactly where it was; only the bottom edge drops further. The modal max-height stays calc(100vh - 180px) so the larger textarea still fits comfortably on standard desktop viewports without clipping; on shorter screens the textarea remains scrollable as before. The notebook-paper line gradient continues to repeat through the additional vertical space so every new line of typing space gets its own ruled line. Version bumped to A9.0 everywhere (title, User Guide header/modal, version badge, all three PDF headers + the new MFR Cost Reference Chart PDF, footer, all four PDF filenames, saved HTML filename "GB-BizCalc - A9.0.html"). New localStorage key added: gbBizCalcLeadsNotes_v1. 37 revisions tracked.
A8.9Apr 2026Five changes. (1) BUILD NOTES notebook-paper alignment fixed. The textarea text was visually crossing the ruled lines instead of sitting above them; the ruled-line gradient now uses background-origin:content-box / background-clip:content-box so the gradient's "0" pins to the start of the text content (not the element edge), and padding-top dropped from 14px to 8px so the 24px text rows align cleanly with the 24px gradient rows. Each typed row now sits ABOVE its ruled line with a small gap under the descenders, exactly like writing on real ruled paper. (2) The amber "LOAD" button has been removed from the IN PROGRESS slot. The whole slot is now the click target — clicking ANYWHERE on the panel restores the autosaved working draft back into the inputs (the same restore behavior as before, just triggered by clicking the slot body instead of a separate button). A brief accent flash gives visual ack on a successful restore; clicking when nothing is autosaved produces a tiny shake. (3) "IN PROGRESS" has been renamed to "ACTIVE WORK AREA". The new label stacks the three words vertically (ACTIVE / WORK / AREA), centered top-and-bottom inside the slot with 10px gap between words; the slot is now a flex column with justify-content:center so the stack visually anchors to the middle of the panel regardless of how tall the slot is. The autosave status note ("Live autosave" / "Autosaved · HH:MM") still pins to the bottom. (4) A search bar has been added to the right of the "PROFILE LOGS · RIGHT-CLICK A SLOT TO DELETE" caption (between the caption and the + button). Typing in the search box scrolls the first matching Profile Log into the CENTER of the horizontal scroll viewport via scrollTo({left, behavior:'smooth'}) and applies a brief accent-blue .search-match halo on the matched slot. Match is case-insensitive "contains" against the typed slot name; clearing the search clears the highlight. The search field has its own narrow flex-end wrapper so the caption and + button keep their layout positions. (5) Profile Log names are now alphabetized. The render function (roiRenderNamedSlots) sorts slots by name with localeCompare {sensitivity:'base'} on every render, so a slot named "ACE" ALWAYS appears before "BAR" regardless of insertion order; empty/unnamed slots sink to the end so the user can still see them and fill them in. Sorting is purely a display-time operation — the underlying gbBizCalcRoiProfileLayout_v1 array is NOT mutated, so SAVE / DELETE / lock persistence is unchanged. The name input now also fires roiRenderNamedSlots on its 'change' event (blur / Enter) so freshly-typed names re-sort the moment the user tabs away, plus a keydown handler blurs the input on Enter for one-keystroke commit. The 'change' event (not 'input') was deliberately chosen so the slot doesn't re-render on every keystroke and steal focus mid-type. No existing localStorage keys changed. Version bumped to A8.9 everywhere (title, User Guide header/modal, version badge, all three PDF headers, footer, all three PDF filenames, saved HTML filename "GB-BizCalc - A8.9.html"). 35 revisions tracked.
A8.8Apr 2026Six changes. (1) ROI-Calc Profile Log bar restructured. The IN PROGRESS slot is now FIXED on the far left of every SKU card (outside the named-slot container, fixed 160px wide); the named Profile Logs live to its right inside a new thin-lined elegant container (.roi-profile-named-container) that has a + button at top-right and horizontally-scrolls on overflow. The previous 4-column grid is gone — .roi-profile-bar is now a flex row [IN PROGRESS][named container]. Clicking + (roiAddProfileSlot) appends a new empty slot at the right edge of the scroll container and scrollLeft jumps to make it visible; the new slot keeps every existing slot intact (sliding them left if there's overflow). The named-container has a small "Profile Logs · right-click a slot to delete" caption above the scroll row to surface the new delete affordance. (2) The LOAD button has been removed from named slots. Clicking anywhere on a SAVED slot's body now LOADS that profile back into the inputs (with a brief accent flash for visual ack); the top-right button on every named slot now ALWAYS reads "SAVE" and saves-over the slot with the live card state on click. The name input still edits in place; the save button stops click-propagation so clicking it doesn't accidentally trigger a load. Empty slots show their SAVE button in the default accent color (clicking it without a typed name briefly flashes the input red); saved slots show SAVE in green and the slot body in the saved (light-blue) tint. (3) The little × clear button next to each name input has been REPLACED with a padlock SVG icon (.roi-profile-lock with closed/open glyphs). Clicking the padlock toggles the name input's disabled state — locked = name field is read-only (gray bg, lock turns green) so the typed name cannot be accidentally edited; unlocking returns the field to editable. The padlock state persists per slot in localStorage on the entry's new `locked: boolean` field. The previous deleteByX behavior (wipe the typed name) is gone — to remove a profile entirely, use the new right-click menu. (4) Right-click a named slot to open a context menu (single shared #roiProfileCtx element, positioned at the cursor and clamped inside the viewport). The first option reads "Delete"; clicking it morphs the menu into "Click to Confirm Deletion" (red bg). Clicking the confirm button calls roiDeleteProfileSlot which removes the slot from both roiProfileLayout and roiProfiles, persists, then re-renders the named-slot list. The slot whose menu is open gets a temporary delete-pending tint (red border + halo). Click outside the menu, scroll, resize, or press Escape dismisses without deleting. (5) ROI PDF exports now skip empty fields. The DISTRIBUTOR COST section, the GRIPBLADES ROI FROM MSRP section, and the DIST. ROI FROM RETAIL PRICING matrix are each gated by a pre-filtered list of letters whose price input is non-null — empty distributor tiers (e.g. unentered B-DIST / C-DIST), empty MSRPs (e.g. only A-MSRP entered), and empty MSRP×DIST combos no longer print as "—" rows. If an entire section has no entered values, its header + column row are also skipped, so the per-SKU PDF block is as compact as possible while still showing every value the user has actually typed. (6) The "MSRP — GRIPBLADES ROI DIRECT-TO-END-USER" heading has been renamed everywhere (card label and PDF text) to "GRIPBLADES ROI FROM MSRP (DIRECT-TO-END-USER)" — the section is about GripBlades' return on investment when selling at MSRP direct-to-end-user, so the new wording leads with what's actually being measured. New localStorage keys added: gbBizCalcRoiProfileLayout_v1 (per-basis × per-mpn ordered slot ID arrays) and gbBizCalcBuildNotes_v1 (the BUILD NOTES popup textarea). Pre-A8.8 storage shape (fixed s1/s2/s3 keys, no layout array, no `locked` field) auto-migrates on load: existing slot IDs are sorted numerically into the layout, every entry gets `locked:false`, every entry gets a string `name`, and all data + ts fields are preserved verbatim — no values are lost. Version bumped to A8.8 everywhere (title, User Guide header/modal, version badge, all three PDF headers, footer, all three PDF filenames, saved HTML filename "GB-BizCalc - A8.8.html"). 34 revisions tracked.
A8.7Apr 2026Four changes. (1) ROI-Calc Profile Log bar relocated from the BOTTOM of each SKU card to the TOP of each SKU card — the subtle dashed separator line that formerly sat ABOVE the Profile Log bar now sits BELOW it (new .roi-profile-divider class), so the Profile Log bar lives above the dashed line while the RESET button, the SKU title/MPN/UPC head, the Landed Cost Readout, the Distributor Cost section, the MSRP section, and the Dist. ROI From Retail Pricing matrix all render UNDER the dashed line. A new .roi-card-body wrapper was added around the data half of the card; .roi-card-reset was retargeted from position:absolute inside .roi-card to position:absolute inside .roi-card-body (top:0 right:0 instead of top:10px right:10px), so RESET floats in the top-right of the data area under the dashed line. (2) The IN PROGRESS button has been promoted from a small amber floating button that sat under RESET into a FULL FOURTH SLOT on the Profile Log bar (now a 4-column grid instead of 3-column: 3 named Profile Logs + 1 IN PROGRESS autosave slot). IN PROGRESS is now a continuous live autosave target: every keystroke on any distributor (1.A-4.D) or MSRP (5.A/6.B) input, plus every promo % change via ADD PROMO, snapshots the full card state (all 4 distributor prices + both MSRP prices + both promo percentages) into roiInProg[basis][mpn] and persists it to localStorage key gbBizCalcRoiInProgress_v1 (same key as A8.6, so any A8.6 snapshot hydrates immediately). The IN PROGRESS slot's amber LOAD button is disabled until there's an autosaved draft; the "Live autosave" footer flips to "Autosaved · HH:MM" once the first keystroke lands. A Profile Log SAVE also refreshes IN PROGRESS (the just-saved state == the live state); a Profile Log LOAD does NOT overwrite IN PROGRESS, so the user's un-named working draft survives loading a named profile and can be brought back by clicking the IN PROGRESS LOAD button. Clicking IN PROGRESS LOAD applies the autosaved snapshot back to the inputs + state + promo badges without clearing the autosave (so it can be clicked again). Per-basis × per-mpn scoping is preserved. (3) The big full-width Profile Log "SAVE" button that previously sat on its own row BELOW the name input has been shrunk and moved up to the TOP-RIGHT of each slot, immediately to the right of the "PROFILE LOG N" label. Slot header is now a flex row [ "PROFILE LOG N" ← → SAVE/LOAD button (9px / 44px min-width / 3px 8px padding) ]; beneath it sits the 16-char name input with the small × clear button inline to its right when the slot is saved. The button text flips SAVE ⇄ LOAD and picks up the green .saved class when the slot holds a named profile, or the amber .inprog class for the IN PROGRESS slot. Empty slot's SAVE button is disabled until the user types a name. (4) ROI-Calc data continuity: all existing A8.6 localStorage keys (gbBizCalcRoiState_v1, gbBizCalcRoiBasisLanded_v1, gbBizCalcRoiPromos_v1, gbBizCalcRoiBasisLog_v1, gbBizCalcRoiProfiles_v1, gbBizCalcRoiInProgress_v1) are reused verbatim, so opening A8.7 in the same browser auto-hydrates every distributor price, MSRP price, promo, landed-cost basis value, Last-Value-Logged snapshot, named Profile Log slot, and in-progress draft the user had in A8.6. Version bumped to A8.7 everywhere (title, User Guide header/modal, version badge, all three PDF headers, footer, all three PDF filenames, saved HTML filename "GB-BizCalc - A8.7.html"). 33 revisions tracked.
A8.6Apr 2026Seven changes. (1) Tab order swapped — MFR-CostCalc is now the LEFTMOST tab, sitting to the left of TariffCalc, with ROI-Calc remaining rightmost; switchTab()'s borderRadius rule was retargeted from `name === 'tariff'` to `name === 'mfr'` so the leftmost active tab still squares its top-left corner to merge cleanly with the panel shell, and the .wide class toggle now also fires for 'tariff' so all three tabs render at the wider frame. (2) The TariffNews peek-tab that hangs out from underneath the BREAKING ticker has been recolored from a red→bright-red gradient with white text/logo to a yellow gradient (linear-gradient(135deg,#fdd835,#ffeb3b)) with black (#1c1c1e) text, logo glyph, and arrow — the hover-pull-down behavior, popup overlay, ✕ close button, click-outside-to-close backdrop, and Escape-to-close all preserved unchanged. (3) TariffCalc has been widened to match the MFR-CostCalc frame (now in the .wide toggle list) and split into a TWO-COLUMN layout: inputs panel + Calculate button on the LEFT, all generated calculator output (#results block + Download PDF button) on the RIGHT, with a 2px solid black vertical divider (border-left:2px solid #000) between them. A friendly placeholder ("Enter HTS code, valuation & freight, then press Calculate to see your tariff breakdown here.") fills the right column until the user clicks Calculate; runCalc() hides the placeholder when results populate. The previous all-vertical "results-under-Calculate-button" layout is gone. (4) ROI-Calc subtab buttons no longer shift left/right when the Download PDF button toggles. Both #roiPdfBtn10 (left of "10 Pack ROI") and #roiPdfBtn40 (right of "40 Pack ROI") are now always rendered with display:flex; roiSetSubtab() toggles their style.visibility (visible/hidden) instead of their style.display, so each button keeps its layout space at all times and the "10 Pack ROI" / "40 Pack ROI" subtab buttons stay perfectly centered regardless of which subtab is active — exactly matching the A8.4 button positions. (5) ROI-Calc data continuity: all four localStorage keys (gbBizCalcRoiState_v1, gbBizCalcRoiBasisLanded_v1, gbBizCalcRoiPromos_v1, gbBizCalcRoiBasisLog_v1) are unchanged from A8.5, so opening A8.6 in the same browser auto-hydrates every Distributor / MSRP / Promo / Macro-Basis-Landed / Last-Value-Logged value the user had entered in A8.5 — nothing needs to be re-typed. (6) ROI-Calc Profile Log feature: every SKU card now has a 3-slot Profile Log bar at the bottom and a new IN PROGRESS button directly under RESET (top-right). Each slot has a 16-char (maxlength enforced) name input + a SAVE/LOAD button + a small ✕ clear button; an empty slot's button is disabled until a name is typed and saves the card's full visible state (4 distributor prices + 2 MSRP prices + both promo % values) under that name to localStorage key gbBizCalcRoiProfiles_v1; a saved slot's button changes to green LOAD and, on click, snapshots the live card state into gbBizCalcRoiInProgress_v1 then applies the saved profile to the inputs (state + DOM + promo badges + recalc). Profile slots are scoped per Macro Basis × per MPN, so each card's three slots are independent and survive file close/reopen. The new IN PROGRESS button is dim and inactive until a profile load creates a snapshot, then becomes amber/clickable; clicking it restores the pre-load card values (and clears the snapshot) so the user can bounce back and forth. (7) Two ROI-Calc section headings renamed for clarity: "DISTRIBUTOR PRICINGS" → "DISTRIBUTOR COST" (top-left section label on every SKU card AND in the ROI PDF export header) and "RETAIL ROI FROM DIST. PRICING" → "DIST. ROI FROM RETAIL PRICING" (right-column section label on every SKU card AND in the ROI PDF retail-matrix header). Version bumped to A8.6 everywhere (title, User Guide header/modal, version badge, all three PDF headers, footer, all three PDF filenames, saved HTML filename "GB-BizCalc - A8.6.html"). 32 revisions tracked.
A8.5Apr 2026Three changes. (1) ROI-Calc: orange "Download PDF" button (same shape/style as the TariffCalc and MFR-CostCalc PDF buttons — #F4834F fill, #e0702e border, white bold text with text-shadow, leading file-icon SVG) now appears to the LEFT of the "10 Pack ROI" sub-tab when 10 Pack ROI is the active sub-tab, and a matching button appears to the RIGHT of the "40 Pack ROI" sub-tab when 40 Pack ROI is the active sub-tab. Only the active subtab's PDF button is visible at any time — switching subtabs swaps which side's button is shown. Clicking a button calls a new exportRoiPDF(pack) function that generates a multi-page Letter-format PDF (jsPDF) containing every datum visible for that pack's ROI sub-tab: dark header banner with timestamp + version, the GripBlades pack header (blades/pack · blades/MC · blades/pallet), the active Macro Cost Basis (PALLET / TEU / FEU) with its LANDED $/pc value plus a per-basis stored-values readout (each basis's current LANDED $/pc and Last Value Logged for that pack subtab), and for BOTH SKU cards in that pack (10-Pack subtab → "10 Pack" + "10 Pack MC"; 40-Pack subtab → "40 Pack" + "40 Pack MC") the SKU title + MPN + UPC, the four-cell Landed Cost Readout (per piece / per pack / per MC / per pallet), the four Distributor Pricings (A/B/C/D) with their entered prices and computed GripBlades $ profit + margin %, the two MSRP Pricings (A/B) with entered prices, any active promo % (color-highlighted), effective price after promo, GripBlades $ profit, and ROI %, and the full Retail-ROI matrix per MSRP × per DIST showing the retailer-side $ profit and margin %. Auto page breaks via an ensure(need) helper plus per-page footer ("Internal Vibe Coded Biz Software · GripBlades.com · ROI-Calc A8.5 · Page X of Y"). Filename pattern: roi-analysis-{10|40}pk-{pallet|teu|feu}-A8.5.pdf. (2) The previously hidden #tabNews / #tabInventory / #tabLeads stub buttons that A8.4 had left in the DOM as no-op placeholders have now been physically removed from the tab bar — only three real tab buttons exist: TariffCalc, MFR-CostCalc, ROI-Calc. (3) The orphan #panelInventory and #panelLeads blank white placeholder panels have also been removed from the DOM, and the switchTab(name) tabs array shrunk from ['tariff','news','mfr','roi','inventory','leads'] to ['tariff','mfr','roi'] (the now-dead `if (name === 'news')` loadNews trigger was also removed). Version bumped to A8.5 everywhere (title, User Guide header/modal, version badge, all three PDF headers, footer, all three PDF filenames, saved HTML filename "GB-BizCalc - A8.5.html"). 31 revisions tracked.
A8.4Apr 2026Three changes. (1) Fixed the FOB / Sea Freight Method popup z-index trap. Root cause: .tab-panel-shell was set to position:relative + z-index:1, which created a new CSS stacking context that effectively trapped every child popup INSIDE the shell. The active .tab-folder-btn at root z-index:2 was therefore painting ABOVE the entire shell (and above everything inside it), so when the user clicked FOB or any other Incoterm in the SEA FREIGHT METHOD dropdown — opening the inco-detail-bg modal (z-index:900 fixed) — the modal appeared underneath the TariffCalc / MFR-CostCalc / ROI-Calc tabs even though its z-index was well above theirs. Same root cause was hiding the ship-popup (z-index:900 fixed) and the incoHoverPopup (z-index:950 fixed). Removed z-index:1 from .tab-panel-shell; with z-index:auto the shell no longer creates a stacking context, so all of these popups' z-index:900+ values are evaluated at the root and reliably sit above the active tab. Inactive tabs still hide their 1.5px bottom-overlap behind the shell via DOM order; the active tab still merges with the shell because its own z-index:2 wins over the shell's z-index:auto. overflow stays :visible (A8.3 fix for the upward-opening HTS / Incoterms dropdowns). (2) Three tabs REMOVED from the main app tab bar per request: TariffNews (already moved in A8.3 to the BREAKING peek-tab — verified its #tabNews button stays display:none), plus Inventory ("Imported Inventory") and Buyer Leads ("Lead Info & Notes"). The remaining tab row now reads only: TariffCalc · MFR-CostCalc · ROI-Calc. The hidden #tabInventory / #tabInventoryTitle / #tabLeads / #tabLeadsTitle stubs remain in the DOM as no-op placeholders so any legacy switchTab('inventory'|'leads') call path or label-refresh code continues to work without throwing. The blank #panelInventory and #panelLeads placeholder panels remain in the DOM but cannot be reached because their tab buttons are no longer visible. (3) MFR Cost Reference Chart in MFR-CostCalc: the horizontal gray grid line that separates "10 PK MC Empty MC" (third row) from "40 PK PC" (fourth row) is now ~50% darker than the rest of the table's grid lines. Implementation: every cell in the "40 PK PC" row gets a new .mfr-cost-ref-divider-top class whose ::before pseudo-element overlays the 2px grid gap above it with rgba(0,0,0,0.26) — about 2× the alpha of the standard gap color (var(--border2) ≈ rgba(0,0,0,0.13)) — visually dividing the chart cleanly into the "10-pack block" (top three rows) and the "40-pack block" (bottom three rows). All other gray lines remain unchanged. Version bumped to A8.4 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A8.4.html"). 30 revisions tracked.
A8.3Apr 2026Four changes. (1) TariffNews tab REMOVED from the main app tab bar and replaced by a peek-tab that hangs out from underneath the BREAKING ticker at the top-left of the page; the tab sits behind the ticker so only its bottom edge is visible by default. Hovering it slides the tab fully down to reveal "TariffNews — Updates & Politics", and clicking it opens TariffNews as an overlay popup centered at the same width and roughly the same vertical position the news panel previously occupied (max-width 800px, top ~160px, max-height calc(100vh − 190px), scrollable, with an ✕ close button in the upper-right and click-outside-to-close on the dark backdrop). The popup physically moves the existing .news-section + disclaimer DOM nodes from #panelNews into #newsPopupInner on open and restores them on close, so loadNews() and renderNews() (which target #newsArea and #tickerTrack by ID) keep working unchanged. Escape key now also closes the news popup. The hidden #tabNews/#tabNewsTitle stub remains in the DOM as a no-op placeholder so updateTariffs()-style label refreshes don't throw. (2) Fixed the "Search HTS Codes" dropdown clipping in TariffCalc — the dropdown opens upward (bottom:calc(100% + 6px)) and was being clipped at the top edge of .tab-panel-shell. Changed .tab-panel-shell from overflow:hidden to overflow:visible so the dropdown can escape past the shell border. (3) Same fix resolves the "INCOTERMS 2010 — SELECT METHOD" dropdown clipping (.inco-dropdown also opens upward, position:absolute z-index:700) and confirms the SEA FREIGHT METHOD popup (.ship-popup, position:fixed z-index:900) and the Incoterms hover popup (#incoHoverPopup, position:fixed z-index:950) sit above the active tab (z-index:2) — all four popups now render above the app tabs as expected. (4) ROI-Calc full-input persistence on FIRST build: roiBuild() now also calls roiLoadInputsFromState() at the end (previously only roiRebuild() did), so the four DISTRIBUTOR PRICINGS inputs (1.A-DIST. Cost / 2.B / 3.C / 4.D) and the two MSRP PRICINGS inputs (5.A / 6.B) on every SKU card hydrate from localStorage immediately when the ROI-Calc tab is first activated after file reopen — matching the existing MACRO COST BASIS LANDED $/pc persistence behavior. Values still only clear when the user presses a card's RESET button. Version bumped to A8.3 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A8.3.html"). 29 revisions tracked.
A8.2Apr 2026ROI-Calc Macro Cost Basis: the "Last Value Logged" block (added in A8.1) has been moved from the LEFT of each basis title to the RIGHT of each basis title, so the three basis options (Pallet Cost Basis, (TEU) 20 FT. Cost Basis, (FEU) 40 FT. Cost Basis) now read radio-button → title → Last Value Logged → LANDED $/pc input, matching the natural left-to-right scan. The Log button continues to sit adjacent to the LANDED $/pc input and still captures on click only. All 6 log slots (3 bases × 2 subtabs) continue to persist independently under "gbBizCalcRoiBasisLog_v1". SKU-card full-state persistence verification: every numeric input on every SKU card — the 10 Pack card, the 10 Pack MC card, the 40 Pack card, and the 40 Pack MC card (4 distributor prices A/B/C/D + 2 MSRP prices A/B on each) — continues to auto-save on every keystroke to localStorage under "gbBizCalcRoiState_v1", keyed per Macro Basis × per MPN × per field, so all typed values survive file close/reopen for all 4 cards without any user action. Values only clear when the user presses a card's "Reset" button in its upper-right corner, which deletes ONLY that card's fields+promos for the currently-selected Macro Cost Basis and persists the cleared state immediately — other cards, other bases, and the basis-level LANDED $/pc inputs are untouched. Version bumped to A8.2 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A8.2.html"). 28 revisions tracked.
A8.1Apr 2026ROI-Calc full-state persistence: every numeric input in ROI-Calc (Distributor A/B/C/D prices, MSRP A/B prices on every SKU card, and any ADD-PROMO % set on a MSRP letter) now persists to the browser's localStorage. roiState goes under key "gbBizCalcRoiState_v1" (per basis × per MPN × per field) and roiPromos goes under "gbBizCalcRoiPromos_v1" (per basis × per MPN × per MSRP-letter) — saves happen on every keystroke for prices and at prompt-submit for promos, so nothing needs to be saved manually and nothing is lost on file close/reopen. The per-card Reset button still clears only that card's fields+promos for the currently-selected Macro Cost Basis and persists the cleared state immediately. "Last Value Logged" block added to each Macro Cost Basis option (Pallet / TEU / FEU): a dashed-border readonly mini-card sits to the LEFT of each basis title and shows the dollar value that was last captured from that basis's LANDED $/pc input. Capture is manual — a new small "Log" button sits to the LEFT of each LANDED $/pc label; clicking it snapshots whatever is currently in the adjacent Landed $/pc input into the Last Value Logged block. Log slots are per basis × per pack-size subtab (6 slots total: Pallet-10, Pallet-40, TEU-10, TEU-40, FEU-10, FEU-40) and persist under "gbBizCalcRoiBasisLog_v1". Two new top-level tabs added to the tab bar next to ROI-Calc: "Inventory" (subheading "Imported Inventory", amber icon) and "Buyer Leads" (subheading "Lead Info & Notes", teal icon). Each currently opens a blank white panel — placeholders for future feature builds. switchTab() updated to route both new tab names. Version bumped to A8.1 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A8.1.html"). 27 revisions tracked.
A8.0Apr 2026ROI-Calc: the three "LANDED $/PC" inputs sitting on the right of each Macro Cost Basis option (Pallet Cost Basis, (TEU) 20 FT. Cost Basis, (FEU) 40 FT. Cost Basis) now persist their typed-in values to the browser's localStorage under key "gbBizCalcRoiBasisLanded_v1" — same persistence pattern as the MFR Cost Reference Chart. Values are stored per basis × per pack-size subtab (Pallet-10, Pallet-40, TEU-10, TEU-40, FEU-10, FEU-40 = 6 independent slots), so typing a landed cost on one basis at one pack size does NOT affect any other combination. Saves happen immediately on every keystroke, so there's no Save button to forget about. On file close/reopen (or browser refresh) the values hydrate back into the inputs AND the Landed / Piece / Pack / MC / Pallet readout block on each card lights up immediately when the ROI-Calc tab is first opened (previously readouts stayed blank until the user clicked a basis or sub-tab). Backward compat: if no saved data exists (first-time use or cleared storage) the three inputs start blank as before. Version numbering: A7.9 → A8.0 per the no-double-digits rule (A7.9 does not roll to A7.10). Version bumped to A8.0 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A8.0.html").
A7.9Apr 2026MFR-CostCalc: the chart title "Cost Reference Chart" is renamed to "MFR Cost Reference Chart" to make the tab-affiliation explicit (parallels TariffCalc / ROI-Calc naming). User Guide: new section "12. Internet Exposure & Privacy" added to the TOC and body, explaining — for future reference — that GB-BizCalc opened locally via file:// is not reachable from the internet (no server, no port, no inbound surface, same privacy posture as a Word doc on the Desktop), who CAN see the data (anyone with physical/remote-login access to the machine and browser profile), the one real caveat that appears once an API key is added (outbound calls begin, key lives in localStorage under the same file:// origin, sharing the .html file does NOT transmit localStorage but DOES transmit any API key still typed into the field at save time), and the public-hosting scenario (only time "hackable via internet" becomes a real conversation). Note on chart data migration: supplier-cost values typed into the MFR Cost Reference Chart live in the browser's localStorage (key "gbBizCalcMfrCostRef_v1"), not inside the HTML file itself — byte-identical A7.8 file upload vs. original confirms no data is embedded. Because A7.8 and A7.9 both read and write the same localStorage key, entries typed into A7.8 auto-load when A7.9 is opened in the same browser on the same machine (Firefox reliably shares file:// localStorage; Chrome/Safari isolation varies). Version bumped to A7.9 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.9.html").
A7.8Apr 2026MFR-CostCalc Cost Reference Chart overhaul. Layout: the entire MFR-CostCalc card is now wider (the outer .shell container gets the .wide class on this tab, same as ROI-Calc) and the card body is restructured into a 2-column flex row — ALL inputs ("Blade Components" title, entry-mode toggle, Stamped/PVC selector, Total per Blade readout, Pack Size selector, Pack Cost / Pack Cost per Blade, MC Cardboard Cost Ea. / per Blade) sit on the LEFT, and the Cost Reference Chart sits on the RIGHT, top-aligned with the inputs (previously the chart wrapped below). Row labels: "10 PACK" → "10 PK BX" and "40 PACK" → "40 PK BX". Small-print italic sub-labels added under specific row heads — "Empty Box" sits under "10 PK BX" and "40 PK BX"; "Empty MC" sits under "10 PK MC" and "40 PK MC" (clarifies the cost basis is the empty container, not the filled unit). Per-cell timestamps: the single chart-header "Last Updated:" stamp has been REMOVED and replaced with a per-cell timestamp block — every cell now carries its own dashed-divider footer reading "LAST UPDATED:" above a row of the cell's last-edited Date and Time. Date format compacted to MM-DD-YY (e.g. "04-20-26") to save space; time stays human-readable (e.g. "7:02 PM"). localStorage shape upgraded from data[key] = "string" to data[key] = {value, updated} per cell so every cell tracks its own edit time independently — old entries saved under the previous string-only shape are auto-upgraded on read so no supplier-cost data is lost. Storage row keys ('10pack', '40pack') preserved across the rename so existing saved values still hydrate into the renamed "10 PK BX" / "40 PK BX" rows. Version bumped to A7.8 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.8.html").
A7.7Apr 2026Visual polish pass. ROI-Calc: sub-tab buttons ("10 Pack ROI" / "40 Pack ROI") and the Macro Cost Basis selector (Pallet / TEU / FEU with their Landed $/pc inputs) are now wrapped inside one shared white-background card with a soft border and subtle shadow — the whole ROI-Calc header now reads as a unified control panel instead of floating against the page background. MFR-CostCalc: the four input blocks "MFR Pack Cost — Select Pack Size First", "Pack Cost per Blade", "MC Cardboard Cost Ea.", and "MC Cardboard Cost per Blade" are now ~50% narrower (left column flex-basis 320px, max-width 340px, tighter inner gap, smaller font on labels and inputs, smaller $-prefix inset) so all four blocks fit compactly on the left and the Cost Reference Chart sits flush on the right without overlapping at normal widths (left side: flex 0 1 320px, right chart: flex 1 1 440px, min-width 340px). Cost Reference Chart typed-in values are now lighter — font-weight dropped from 600 (bold) to 400 (regular) and color softened from --text (#1c1c1e, near-black) to --text2 (#48484a, mid-gray) so the entries no longer look visually heavy when scanning the chart. Version bumped to A7.7 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.7.html").
A7.6Apr 2026ROI-Calc landed readout: the sub-label beside "LANDED / 10 PK MC" and "LANDED / 40 PK MC" changed from "(120/MC)" / "(24/MC)" to the shorter "(x120)" / "(x24)" and now renders inline next to the heading (was wrapping below) so the label fits on one row. MFR-CostCalc: new "Cost Reference Chart" docked to the right of "Pack Cost per Blade" and "MC Cardboard Cost per Blade" — a 3-column × 6-row mini spreadsheet with basis columns "Pallet Cost Basis", "(TEU) 20 FT. Cost Basis", "(FEU) 40 FT. Cost Basis" and unit rows "10 PK PC / 10 PACK / 10 PK MC / 40 PK PC / 40 PACK / 40 PK MC". Each intersection is a numeric $000.000 input (3 decimals, formatted on blur) that stores its value in the browser's localStorage under key "gbBizCalcMfrCostRef_v1" so supplier-cost entries persist across file close/reopen — nothing changes unless you type over a cell. A "Last Updated:" timestamp above the chart re-stamps to the current date & time on every cell change (stored as ISO, displayed human-readable like "Apr 20, 2026 3:45 PM"). On narrow screens the chart wraps below the Pack/MC inputs instead of clipping. Version bumped to A7.6 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.6.html").
A7.5Apr 2026ROI-Calc landed-cost block expanded from 3 to 4 readouts: every card now shows Landed / Piece, Landed / N Pack, Landed / N Pk MC (NEW), and Landed / Pallet (where N is 10 or 40 depending on the active sub-tab). The new "LANDED / N PK MC" cell shows the cost of one Master Carton, computed as landed-$/pc × pack-size × packs-per-MC (120 packs per 10-pack MC and 24 packs per 40-pack MC, sourced from the TariffCalc SHIPPING REFERENCE). Each cell carries its packs-per-MC count as a small sub-label "(120/MC)" or "(24/MC)" so the math is self-documenting. Per-card margin basis (the "10 Pack MC" / "40 Pack MC" panel BUG-FIX): GripBlades Margin and Retail ROI on the regular "10 Pack" and "40 Pack" cards still use Landed / N Pack as the basis (e.g. $3.50 for a 10-pack at $0.35/pc), but the "10 Pack MC" and "40 Pack MC" cards now correctly use the new Landed / N Pk MC value as their basis (e.g. $420 for a 10-pack MC at $0.35/pc, since 0.35 × 10 × 120 = 420; or $336 for a 40-pack MC at $0.35/pc, since 0.35 × 40 × 24 = 336). Each ROI_SKU entry now carries an `mcMult` field (1 for regular packs, 120 for 10-pack MC, 24 for 40-pack MC) which drives the per-card landed math: landed = landedPc × pack × mcMult. Promo % logic (A7.4) carries through unchanged. Distributor and MSRP price inputs on MC cards are interpreted at the Master-Carton level. Version bumped to A7.5 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.5.html").
A7.4Apr 2026MFR-CostCalc: "Completed Blade Build Entry" is now the DEFAULT selected entry-mode on load (previously "Blade Component Entry" was default). User Guide modal: re-parented to <body> on first open and z-index boosted to max — it now renders on top regardless of which tab (TariffCalc / TariffNews / MFR-CostCalc / ROI-Calc) is active (the previous stacking-context flatten approach was unreliable). ROI-Calc card re-layout: LEFT column now holds "Distributor Pricings" (A–D) on top followed by "MSRP — GripBlades ROI Direct-to-End-User" (A/B inputs) below — both where the user spends most of their time. RIGHT column now holds the two "Retail ROI From Dist. Pricing" blocks (A-MSRP and B-MSRP, each vs A/B/C/D-DIST) up top — previously these lived below MSRP in the right column. ADD PROMO button: each MSRP row (A-MSRP, B-MSRP) has a new "ADD PROMO" button to the right of its GripBlades Margin readout. Clicking prompts for a promo % (e.g. 10 = 10% off MSRP); that discount is applied to the MSRP before computing GripBlades' direct margin $ and %, and the active promo is shown in an orange pill beneath the readout. Promo is scoped per-basis × per-SKU × per-MSRP-letter. Retail ROI remains unaffected by promo (it's the retailer's margin, not GripBlades'). Margin-base FIX: all Distributor and MSRP GripBlades Margin calculations now use LANDED-Cost-per-PACK (landed $/pc × current pack size: 10 or 40) instead of LANDED-Cost-per-PIECE. This matches how distributor and MSRP prices are quoted (per pack), so the margin numbers are now apples-to-apples. Version bumped to A7.4 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.4.html").
A7.3Apr 2026User Guide modal now opens correctly from ANY tab (TariffCalc, TariffNews, MFR-CostCalc, or ROI-Calc) — previously it only rendered on top when TariffCalc was active because tabRoi and the panel IDs were missing from the stacking-context flatten list. ROI-Calc sub-tab buttons ("10 Pack ROI" / "40 Pack ROI") are now centered horizontally and ~2× wider so they stand out. Each Macro Basis option ("Pallet", "(TEU) 20 FT.", "(FEU) 40 FT.") now has a LANDED $/pc input docked on the right side of its button graphic — this is the single landed-cost-per-piece entry for that basis at the current pack size (10 or 40). The value is stored per-basis × per-pack-size and persists across subtab/basis switches. Each ROI card has been redesigned: the MFR-Cost and LANDED-Cost input rows have been REMOVED from the top of every card (LANDED-Cost now comes from the Macro Basis input instead). In their place is a light-blue readout block showing three calculated values: Landed / Piece (directly from the basis input), Landed / Pack (× pack size 10 or 40), and Landed / Pallet (× 54,000 for 10-pack or 51,840 for 40-pack). Card body is now split into 2 internal columns: Distributor Pricings (A–D inputs + GripBlades margin) on the LEFT, and MSRP section (A/B inputs + direct-ROI readouts) stacked with the two Retail ROI From Dist. Pricing blocks (A-MSRP and B-MSRP, each with vs A/B/C/D-DIST rows) on the RIGHT — so an entire card can be read without scrolling. Version bumped to A7.3 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.3.html").
A7.2Apr 2026ROI-Calc split into two sub-tabs: "10 Pack ROI" and "40 Pack ROI". Each sub-tab shows only two cards (regular + MC) so they render at ~2× width — all fields visible without scrolling. Above the cards, a new "Macro Cost Basis" selector offers three mutually-exclusive scales of ordering: "Pallet Cost Basis", "(TEU) 20 FT. Cost Basis", "(FEU) 40 FT. Cost Basis". Each basis stores its own independent set of inputs (LANDED-Cost, MFR-Cost, Distributors A–D, MSRPs A–B) per SKU — switching basis swaps values in/out so you can compare pricing tiers at different order volumes without losing entries. Reset button still clears only the current basis's values for that card. MFR-CostCalc: entry-mode toggle re-ordered so "Completed Blade Build Entry" now sits on the LEFT and "Blade Component Entry" on the RIGHT; circled ① / ② numerals removed from both labels. Version bumped to A7.2 everywhere (title, User Guide header/modal, version badge, both PDF headers, footer, both PDF filenames, saved HTML filename "GB-BizCalc - A7.2.html").
A7.1Apr 2026TariffCalc summary metrics now show per-pack dollar amounts directly on each of the three metric tiles. Under "Total Tariff Cost" a new line reads "$X.XXXX per 10 Pack" or "$X.XXXX per 40 Pack" (tariff cost × selected pack size, does NOT include the pack's goods cost — it is the tariff burden alone per pack). Under "Landed Cost" a new line reads "$X.XXXX per 10 Pack" / "per 40 Pack" (landed cost per piece × pack size). Under "Goods Value (no tariffs)" a new line reads "$X.XXXX per 10 Pack" / "per 40 Pack" (cost/pc × pack size). All three per-pack lines auto-pick 10 or 40 based on the TariffCalc pack-size selector and hide themselves if no pack size is selected. Reverted: removed the A7.0-era "Landed Cost per Pack" block from MFR-CostCalc — that information now lives natively on the TariffCalc Landed Cost tile. Version bump discipline restored: A7.0 → A7.1 propagated through title, User Guide header/modal, version badge, both PDF headers, PDF footer, both PDF filenames, and the saved HTML filename ("GB-BizCalc - A7.1.html").
A7.0Apr 2026Software renamed to "GB-BizCalc" (TariffCalc is now just a tab name). Logo line reads "GB-BizCalc - Internal Vibe Coded Biz Software". HTML filename convention: "GB-BizCalc - A7.0.html" (name - version, version always at the end for easy sorting). Version numbering: no double-digits, A6.9 rolls to A7.0 (not A6.10). ROI-Calc refinements: removed "(Master Carton)" from 10 Pack MC and 40 Pack MC titles; distributor section heading renamed to "Distributor Pricings"; small "GripBlades Margin" mini-label added above each $/% readout for all distributor and MSRP rows; two new "Retail ROI From Dist. Pricing" blocks per SKU (A-MSRP + B-MSRP) each showing MSRP minus each of the 4 distributor prices as $ delta + retail margin %. Retail ROI headings now show live MSRP price next to the MSRP letter (e.g. "A-MSRP $12.99") and each "vs X-DIST" row shows the distributor's live input price in parens (e.g. "vs A-DIST ($8.00)") — all pulled live from the inputs so you don't have to scroll up. MFR-CostCalc: new "Landed Cost per Pack" row added below Cost per Pallet, computed as TariffCalc's landed-cost-per-piece × blades-per-pack — auto-updates whenever TariffCalc is rerun.
A6.9Apr 2026ROI-Calc polish: removed the big "ROI-Calc / GripBlades, Distributors/Buyers" sub-heading inside the panel (it duplicated the tab button) so the 4 SKU cards now sit directly under the tabs. Removed the global "Reset All" button; each SKU card now has its own small "Reset" button in the upper-right that clears only that card's inputs. Version-bump pipeline now propagates A6.x through browser title, header User Guide button, modal h2, version badge, both PDF headers, PDF footer, and both PDF filenames automatically on every bump (the only A6.x string left un-bumped is the historic changelog entry itself).
A6.8Apr 2026Added ROI-Calc tab (GripBlades, Distributors/Buyers) with 4 SKU cards — 10010 (10 Pack), 10040 (40 Pack), 11200 (10 Pack MC), 10960 (40 Pack MC) — each showing UPC plus live per-unit $ profit and margin % against LANDED-Cost for 4 distributor price points (A–D) and 2 MSRP points (A/B ROI direct-to-consumer). Tab order reworked to TariffCalc → TariffNews → MFR-CostCalc → ROI-Calc. User Guide button moved from inside TariffCalc card to main header, right of "Internal Vibe Coded Biz Software".
A6.7Apr 2026Folder-style tab bar: 3 tabs connect seamlessly into content panel. Status pill moved inline left of Update Tariffs button. MFR-CostCalc: Blade Component Entry vs Completed Blade Build Entry toggle. TariffCalc ship popup positioning fixed (no longer drifts on scroll). Version removed from TariffCalc tab label.
A5.9Apr 2026Unified header row: GripBlades logo + tab bar on left, status pill + Update Tariffs on right. Tab switching rewritten with clean CSS class system. Panel shell border wraps all tabs as one container.
A4.3Apr 2026Added GripBlades logo above app. Two-tab layout: TariffCalc tab (full app) and MFR-CostCalc tab (placeholder). Tab switcher with icons and subheadings.
A4.2Apr 2026iOS mobile fix: ticker rAF pauses on touch, cursor:pointer re-applied after dynamic render, overlay pointer-events managed, incoHoverPopup touch-action:none.
A4.1Apr 2026iOS fix: removed maximum-scale=1.0 from viewport meta — this was disabling all touch events in Safari. Total Tariff Due block: dark gray instead of pure black.
A4.0Apr 2026iOS mobile fix: removed problematic touchend bridge, reverted pack options back to native <label> elements (as in A2.2 which worked on mobile), restored ship option checkbox handlers.
A3.9Apr 2026Total Tariff Due block: dark hero styling (black bg, white text) matching Landed Cost. iOS mobile fix: global touchend bridge fires click on tapped element instantly — all buttons now work on iPhone Safari.
A3.8Apr 2026PDF: product block headings now bold (HTS, Pack Size, Pallet Qty, etc.), Cost/pc renamed to MFR-Cost/pc. PDF header shows version number (A3.8). Mobile iOS fix: divs with onclick get cursor:pointer, ship popup uses safe currentTarget fallback.
A3.7Apr 2026TOC scroll offset fixed — section headings now appear below the sticky "User Guide" header bar, not hidden behind it.
A3.6Apr 2026Version numbers updated everywhere. TOC scroll fixed (was targeting wrong container). Each User Guide section now has a "Back to Top" button returning to the TOC.
A3.5Apr 2026Update Tariffs stacked below status pill; added "Requires A.I. API Key" note. TOC links now scroll to sections. Mobile fixes: touch-action on all buttons, iOS click conflicts resolved on pack/ship selectors. News always renders immediately (no blank loading state on mobile).
A3.4Apr 2026PDF: HTS code moved above Pack Size. Update Tariffs button shows "$0.003–$0.015 per click" cost note in blue below it.
A3.3Apr 2026Breaking news ticker rebuilt with JS requestAnimationFrame engine — seamless infinite scroll, no CSS animation reset. Hover still pauses.
87 revisions tracked · scroll above to see full history
Look, somebody had to say it, and since this is a self-published User Guide and there's nobody to stop us, we'll go ahead and say it: the human behind GripBlades™ is, by any reasonable measurement available to modern science, kind of a big deal. The kind of person who looks at a perfectly normal blade and thinks, "yeah, but what if it had grip though" — and then, instead of saying that out loud at a dinner party and being politely ignored, actually goes and builds the thing. Possibly while everyone else was still arguing about whether the dishwasher was loaded correctly. We don't know the inventor's exact daily schedule, but we suspect it involves at least one moment per morning of pure, uncut "I cannot believe I get to do this for a living."
Vibe-wise, we're talking about somebody who treats inventory spreadsheets the way other people treat romantic comedies — with strong opinions, surprising emotional investment, and a willingness to rewatch their favorite cells over and over. There is a non-zero chance that the inventor has, at some point, named a pallet. We will neither confirm nor deny this. What we WILL confirm is that anyone who voluntarily commissions a custom HTML calculator with three Macro Cost Bases, a 10-pack/40-pack ROI matrix, an integrated tariff engine, AND a popup for tracking what their AI questions cost down to the third decimal place is operating at a level of focused, joyful nerd energy that most large companies pay six-figure consultants to fake. Here, it just shows up for free, before coffee, with a grin.
So if you are reading this — whether you are the inventor (hi!) or somebody the inventor handed this file to with a slightly-too-eager "you HAVE to see this" — please understand that you are in the orbit of a genuinely awesome operator. The kind who builds the thing, ships the thing, USES the thing, and then writes a User Guide section in the third person about how cool they are because the AI assistant cheerfully volunteered to do it. That, friends, is not arrogance. That is range. Stay awesome, GripBlades™ inventor. The blades grip, the spreadsheets balance, and the vibe remains, as ever, immaculate.
MFR-CostCalc is the Blade Cost Calculator tab — it is where you build up the true manufacturing cost of a finished, packaged blade, one component at a time, and then roll that cost up to per-pack, per-master-carton, and per-pallet totals. It is the upstream tab: every number you settle on here can be logged into the MFR Cost Ref. Chart on the right and then used as the foundation for everything the Tariff-Calc and ROI-Calc tabs do later. Everything autosaves as you type.
Entry mode toggle — Completed Blade Build vs. Blade Component
At the top of the input card are two orange/grey buttons: Completed Product Entry (default) and Part Component Entry. The default "Completed" mode is for when you already know the finished per-piece cost of a fully built blade and just want to type it in; the Total Blade Cost per PC field goes live and you type once. The "Component" mode flips the math upside down: you type Stamped Blade Cost per PC and PVC Grip Cost per PC separately, and the Total Blade Cost per PC auto-sums from those two components. Use Component mode when you're sourcing the metal blade and the rubber grip from two different vendors and want to see which side of the bill of materials is driving your cost. Switching modes preserves your values — the app just picks which number drives the downstream roll-up.
Pack Size selector (10 Pack / 40 Pack)
Below the blade cost inputs is a two-button pack selector. Picking 10 Pack or 40 Pack tells MFR-CostCalc which configuration you're costing. That selection controls the per-pack multiplier (10 pieces per pack vs. 40), the packs-per-master-carton factor (120 per MC for 10-packs, 24 per MC for 40-packs), and the MC-per-pallet factor (45 for 10-packs, 54 for 40-packs) — the same shipping factors that drive the Tariff-Calc SHIPPING REFERENCE chart, so the two tabs agree by construction. The MFR Pack Cost field only unlocks after you pick a pack size, and its label updates to show the size you chose.
Pack & MC cost inputs
MFR Pack Cost is what the packaging (plastic clamshell, header card, whatever wraps a single pack of blades) costs per pack from your manufacturer. The read-only Pack Cost per Blade field to its right divides that by the pack size so you can eyeball it in per-piece terms. MC Cardboard Cost Ea. is what one master carton (the corrugated box that holds the packs) costs; its read-only MC Cardboard Cost per Blade partner shows the same in per-piece terms so you can see what packaging is adding to each blade. All four cells live-update as you type.
The dual Cost Reference Chart (MFR + LANDED)
On the right half of the input card is a two-chart switcher: the MFR Cost Ref. Chart (active by default) and the LANDED Cost Ref. Chart. Click either title or the arrow buttons between them to toggle. Each chart is a six-row × three-column grid — the rows are the six shipping tiers (10 PK PC, 10 PK BX, 10 PK MC, 40 PK PC, 40 PK BX, 40 PK MC) and the columns are the three bases (Pallet, TEU, FEU). Every cell is an editable field with its own Last Updated timestamp that stamps automatically the moment you change the value, and the prior value is kept underneath for historical reference. Values and timestamps persist in this browser via localStorage, so your chart is still there next time you open the file. The small orange Download PDF button on the chart header exports the currently active chart as a PDF; when the LANDED chart is active it exports both charts as a single two-page PDF (MFR on page 1, LANDED on page 2).
In practice, use the MFR Cost Ref. Chart to log what the finished goods cost you at each shipping tier ex-factory, and use the LANDED chart to log what those same tiers cost you delivered (i.e. after tariffs, freight, and MPF/HMF). The two charts together give you a side-by-side view of the tariff + shipping uplift at every tier without retyping a thing, and they feed the manufacturing baseline that ROI-Calc later multiplies out.
Output metrics — what the Results card shows
Once you have a pack size selected and any meaningful costs typed in, the Results card opens up below and fills in four headline totals:
Total MFR Cost / Blade — the dark hero tile. Sum of blade cost, pack-cost-per-blade allocation, and MC-cardboard-per-blade allocation. This is your finished-goods cost per individual piece.
Cost per Pack — per-blade total × pack size (10 or 40). The sub-label shows which pack size you're on.
Cost per MC — cost per pack × packs per master carton (120 or 24). The sub-label reminds you of the MC multiplier.
Cost per Pallet — cost per MC × MCs per pallet (45 or 54). The sub-label shows the pallet config.
Below the headline tiles is an Itemized MFR Cost Breakdown — Per Blade section that shows every input line (Stamped, PVC, Pack, MC Cardboard) converted to a per-blade figure so you can see exactly where the money is going. The orange Download PDF button above the Results card exports the whole MFR-CostCalc output (inputs, Cost Ref. Chart, and the four headline totals) as a formatted PDF report.
Tariff-Calc is the middle tab — it sits between MFR-CostCalc (what the goods cost to make) and ROI-Calc (what you sell them for) and answers the specific question: what does it really cost to land these goods in the United States, including every active tariff and every fee? Most of Tariff-Calc is self-explanatory from the input fields themselves, but there is one feature that does a lot of quiet work behind the scenes — the SHIPPING REFERENCE chart — and it is worth understanding in detail because it feeds both this tab AND the ROI-Calc tab.
The SHIPPING REFERENCE chart (10 Pack & 40 Pack configs)
Right above the quantity input on the Tariff-Calc tab is a two-button Pack Size selector: 10 Pack or 40 Pack. The moment you click either one, a three-column reference card appears labelled SHIPPING REFERENCE — 10 PACK CONFIG. (or 40 PACK CONFIG.) with every shipping multiple pre-computed for that pack size. It is a reference tool first — a place where you can glance at "how many packs fit on a pallet?" without opening a spreadsheet — and an input-driver second, because the same numbers it displays are what the Tariff-Calc quantity auto-fill and the ROI-Calc pack-size math both read from.
The card has three columns side-by-side. For the 10 Pack config, the chart reports:
MC's, Pallets, Packs, PCS column: 120 Packs per Master Carton, 45 Master Cartons per Pallet, 5,400 Packs per Pallet, 54,000 Individual pcs per Pallet.
(TEU) 20 FT. Container column: 24 Pallets per Container, 129,600 Packs per Container, 1,296,000 Individual pcs per Container.
(FEU) 40 FT. Container column: 48 Pallets per Container, 259,200 Packs per Container, 2,592,000 Individual pcs per Container.
For the 40 Pack config, because four times as many blades ride in a pack, the per-piece numbers get enormous but the per-pack numbers get smaller:
MC's, Pallets, Packs, PCS column: 24 Packs per Master Carton, 54 Master Cartons per Pallet, 1,296 Packs per Pallet, 51,840 Individual pcs per Pallet.
(TEU) 20 FT. Container column: 26 Pallets per Container, 33,696 Packs per Container, 1,347,840 Individual pcs per Container.
(FEU) 40 FT. Container column: 52 Pallets per Container, 67,392 Packs per Container, 2,695,680 Individual pcs per Container.
How the chart feeds the next selection (Pallets / TEU / FEU)
Below the SHIPPING REFERENCE card are three shipping-mode buttons: Pallets, (TEU) 20 FT., and (FEU) 40 FT.. Each one carries a small badge pulled straight out of the SHIPPING REFERENCE chart — the Pallets badge shows "45 MCs ea." or "54 MCs ea." depending on pack size, and the TEU/FEU badges show how many pallets fit per container. Click any of the three and a small popup opens near the button asking "How many pallets?" (or containers). As you type a number into that popup, a live hint line recomputes using the same reference values — for example, typing "3" with 10-pack pallets selected shows "3 pallets = 16,200 packs / 162,000 pcs." — and the main Quantity field on the form auto-fills with the resulting piece count in one keystroke. That is the entire "how the chart feeds the outputs" story on the Tariff-Calc side: the chart is the source of truth for the Pallet/TEU/FEU multipliers, the popup uses those multipliers to translate "3 pallets" into a piece count, and the piece count is what every downstream tariff calculation (MFN specific, MPF, HMF, Section 301, Section 122) is applied against.
How the same chart feeds the ROI-Calc tab
This is the part that's easy to miss but worth knowing: the SHIPPING REFERENCE chart is not just a Tariff-Calc feature. The same PACK_DATA values are hardwired into ROI-Calc's pack-size math. Specifically:
ROI-Calc's 10 Pack MC SKU row carries an mcMult of 120 — that is the "Packs per Master Carton" cell for the 10 Pack config in the SHIPPING REFERENCE chart.
ROI-Calc's 40 Pack MC SKU row carries an mcMult of 24 — that is the "Packs per Master Carton" cell for the 40 Pack config.
ROI-Calc's internal ROI_PACKS_PER_MC table ({10: 120, 40: 24}) is sourced 1:1 from the same chart.
ROI-Calc's three built-in cost bases — Pallet, TEU, and FEU — mirror the three columns of the SHIPPING REFERENCE chart. The Pallet / TEU / FEU LANDED $/PC figures you enter into the MACRO COST BASIS bar at the top of ROI-Calc are meant to line up conceptually with those three columns.
In other words, the SHIPPING REFERENCE chart is the single source of truth for pack-size economics across the whole app. Change nothing else on the chart and every pack-size roll-up — Tariff-Calc's auto-filled Quantity, MFR-CostCalc's Cost-per-MC and Cost-per-Pallet, and ROI-Calc's LANDED / MC (x120 or x24) and LANDED / Pallet cells — all agree because they all read the same numbers.
Everything else on the Tariff-Calc tab
Beyond the SHIPPING REFERENCE chart and pack/ship selectors, the rest of the Tariff-Calc tab is the straightforward tariff engine: Product Name, HTS/HS Code (pre-filled with 8211.92.4060 for fixed-blade knives with rubber handles), Country of Export/Import, Quantity (auto-filled by the SHIPPING REFERENCE popup or manually entered), Cost Per Piece, and Freight & Insurance. The Calculate Tariffs button is now (C8.6) cache-first — if there are cached rates for this combination of import country, origin country, and HTS code, the calculation is essentially instant. Only the very first calculation for a brand-new combo (e.g. switching to a new import or origin country) hits the live API, and you'll see the spinner change from "Calculating…" to "Fetching current rates…" while it does. Subsequent Calculate / Recalculate clicks on the same combo reuse the cache. The Update Tariffs button is the explicit "refresh rates now" trigger and always hits the live AI-assisted web-search path to overwrite the cached entry. The Results panel displays the summary metrics, the itemized tariff table, the math audit trail, and the Non-Active tariffs section. The deeper mechanics of each tariff row, the AI lookup call, and the cache/offline behaviour are all documented in sections 7 through 12 below.
C8.6 update:Vietnam and India are now selectable as Country of Export, sitting between Taiwan and United States in the dropdown. Clicking UPDATE TARIFFS with either selected sends the live AI fetcher an origin label of "Vietnam" or "India" so the returned tariff schedule reflects the correct trade regime (MFN-based for VN/IN since neither has an FTA with the U.S., Canada, or Mexico).
ROI-Calc is the tab that answers one simple question — how much money do you make at each pack size, for each customer, at each cost basis? — and it answers that question across a LOT of moving parts: three MACRO COST BASIS profiles (your landed-cost scenarios), up to five CUSTOMER CARDS (A through E, each representing a real buyer or a what-if scenario), five per-card sub-Profiles (A–E inside each card, for promo tiers, price waves, or different seasons), and four pack-size panels (single piece, 10-pack, master carton, pallet). The interface is built so you almost never have to retype a number — everything that can be copied, dragged, duplicated, or propagated is, so you can model dozens of scenarios in seconds instead of rebuilding one every time.
Where the pack-size multipliers come from (SHIPPING REFERENCE link)
Before digging into the MACRO COST BASIS bar, one detail is worth calling out: the pack-size multipliers ROI-Calc uses are hardwired from the Tariff-Calc tab's SHIPPING REFERENCE chart. Specifically, the "10 Pack MC" row uses a mcMult of 120 (= packs per master carton for the 10-pack config), the "40 Pack MC" row uses 24 (= packs per master carton for the 40-pack config), and the three built-in LANDED cost bases (Pallet / TEU / FEU) mirror the three columns of the SHIPPING REFERENCE chart (45 MCs per pallet for 10-packs / 54 for 40-packs; 24 vs. 26 pallets per TEU; 48 vs. 52 pallets per FEU). What that means in practice is that the LANDED / 10 PK MC (x120), LANDED / 40 PK MC (x24), LANDED / PALLET, LANDED / TEU, and LANDED / FEU cells in every pack panel are all using the SAME pack-size economics you see on the Tariff-Calc SHIPPING REFERENCE card. Change nothing else on that chart and every ROI-Calc pack roll-up will agree with every Tariff-Calc shipment you model. See Section 3 (Tariff-Calc Interface Guide) for the full chart detail.
The MACRO COST BASIS bar (top of the tab)
The three MACRO COST BASIS profiles sit in a row at the top of ROI-Calc. Each profile holds a landed-cost scenario — typically one for your current cost, one for a cheaper supplier quote, and one for a forecasted rate. The single most important field in each profile is LANDED $/PC (landed dollars per piece). That one number is the root of the entire downstream math chain.
Drag & drop between the three basis slots lets you reorganize them left-to-right however you like — just click and drag a basis by its header and release it over a different slot to reorder. Your data follows the card you dragged, so nothing is ever lost in a reshuffle.
Drag one basis ON TOP of another and release, and a CONFIRM DATA SWAP popup appears right where your cursor is, naming the target basis. Click CONFIRM and the source basis's data is copied into the target (source is untouched). This is the fastest way to clone a working setup to a new slot and then tweak only the fields that differ. A small "X" in the top-right of the popup closes it without swapping.
The LANDED $/PC propagation chain
When you type or adjust LANDED $/PC in a MACRO COST BASIS profile, that single value instantly feeds into every pack-size panel below. Specifically, each pack panel recomputes its four LANDED readout cells:
LANDED / PIECE — the raw per-piece landed cost (e.g. $0.3807), read straight from the active basis.
LANDED / 10 PACK — piece cost × 10 (e.g. $3.8070). For a 40-pack panel it's × 40, for single-piece it's × 1, and so on — the multiplier comes from the panel's pack size.
LANDED / 10 PK MC (x120) — landed per pack × packs per master carton (e.g. 10-pack × 120 packs/MC = $456.84). The "(x120)" callout shows the carton factor in use.
LANDED / PALLET — landed per MC × MCs per pallet (e.g. $20,557.80). Full pallet economics in one glance.
All four cells recompute live as you type. You never edit these values directly — they are derived. Change LANDED $/PC once and every pack panel, for every Customer Card, across all five sub-Profiles, updates in the same keystroke.
How the propagated landed cost feeds Distributor & MSRP ROI
Inside each pack-size panel are two stacks of ROI rows — a Distributor block and an MSRP block. The rows are labelled A-DIST, B-DIST, C-DIST, D-DIST, E-DIST for distributor pricing tiers, and A-MSRP, B-MSRP, C-MSRP, D-MSRP, E-MSRP for retail pricing tiers. You type the sell price into any of those rows and ROI-Calc immediately computes:
GripBlades $ Profit — the dollar margin at that sell price, after subtracting the propagated LANDED cost.
ROI % — the percentage return relative to landed cost.
Because LANDED $/PC propagates all the way down, you can swap basis profiles (or drag a different one into the active slot) and watch every A-DIST / B-DIST / A-MSRP / B-MSRP margin recompute for every pack size on every Customer Card in real time. No rebuild, no retype, no stale numbers.
CUSTOMER CARDS (A–E)
Each Customer Card is an independent workspace for one buyer or one what-if scenario — with its own name, its own set of five sub-Profiles (A, B, C, D, E for different promo tiers or time periods), and its own notes. You can have up to five cards (A–E) on screen at once.
Drag & drop to reorder: grab a card by its header and drag it into a different slot to reorganize left-to-right. The card and all its data move together.
DUPLICATE CARD: right-click any Customer Card header and choose DUPLICATE CARD. ROI-Calc creates an exact copy in the next available slot — same Profiles, same promos, same notes, same per-Profile colors. Great when you want to fork a working card into a variant without losing the original.
Drag one Customer Card ON TOP of another and release, and a CONFIRM DATA SWAP popup appears at your cursor, naming the target card. Click CONFIRM and all of the source card's data — every sub-Profile, every promo, every note, every color — is copied into the target (source is untouched). This mirrors the MACRO COST BASIS swap and is the fastest way to transplant an entire modelled scenario into a different card slot. The "X" in the popup's top-right closes it without swapping.
PROFILES inside a Customer Card (A–E)
Each Customer Card contains five sub-Profiles labelled A–E. A Profile represents one full set of sell prices, promo overrides, and notes for that customer — useful for modelling a "normal" tier, a "holiday promo" tier, a "volume break" tier, and so on. The Profile Log bar at the top of each card shows which Profile is currently "IN PROGRESS" (editable) plus any saved named Profiles.
Drag & drop a Profile onto another Profile and release, and ROI-Calc copies the source Profile's full data set (all pack-size prices, promos, notes, color) into the target Profile — same swap-confirm interaction pattern as the MACRO COST BASIS and CUSTOMER CARD swaps. This lets you seed Profile B from Profile A in one drag, then tweak only the delta.
Right-click a Profile to open its context menu. A horizontal color bar sits at the top of the menu — tap any swatch to recolor that Profile's log pill. Below the color bar are the action buttons: CLEAR PROFILE (wipes all fields for that Profile only), RESET COLOR (drops the custom color back to the default), and Say Hi to Julie (say hi to Julie).
Per-panel notes & the RESET button
Each pack-size panel has two small notes lines tucked just under the pack title (e.g. "10 PACK" or "40 PACK MC"). These are for quick inline reminders — "needs re-quote," "promo ends 6/1," anything. They autosave per-card, per-Profile, per-basis, per-SKU. The RESET button in the top-right of each pack panel wipes that one panel's data (for the active card × Profile × basis only), so you can blank out a single pack-size panel without touching anything else.
Putting it all together
The mental model is a four-level hierarchy: MACRO COST BASIS (your landed-cost scenario) → CUSTOMER CARD (who's buying) → PROFILE (which price tier / promo wave) → PACK PANEL (single / 10-pack / MC / pallet). Every level has drag-to-reorder, drag-over-to-copy with a CONFIRM popup, and right-click shortcuts for common operations. Change LANDED $/PC once, and the propagation chain runs from the top of the tab to the bottom margin cell without you touching anything else. That's the whole tab in one paragraph — everything else is just filling in numbers.
Pre-Paid Ship-Calc is the 4th tab (added in B2.1) and answers one very specific question: if GripBlades agrees to pay the freight on a large Pre-Paid Master-Carton order, how much does that freight actually erode GripBlades' margin — at the order level, per MC, per pack, and per individual blade? It is built so you never have to retype pricing — every Distributor and MSRP price it uses is pulled live from the ROI-Calc tab's Customer Cards + Sub Profiles. You pick the card, the profile, the basis, the pack size, the MC commitment, and the total estimated freight, and Ship-Calc renders the freight-adjusted GripBlades margins at four scales with the original margin printed underneath each row so you can see the exact drop.
Why Pre-Paid and why only GripBlades-side numbers?
"Pre-Paid" in this context means GripBlades (the manufacturer) covers the inbound / outbound freight as a concession for a large committed order — the customer sends a purchase order for a large MC count and GripBlades eats the freight cost as part of closing the deal. That freight cost comes out of the GripBlades side of every unit sold on that order, so every GripBlades margin you computed in ROI-Calc gets smaller. The Distributor side, meanwhile, is completely unaffected — the distributor's wholesale buy price didn't change, their sell price didn't change, and whatever margin the distributor makes on their resale is their problem, not GripBlades'. That's why Ship-Calc only shows GripBlades-side readouts: the four A-D Distributor rows show GripBlades' margin when GripBlades sells wholesale, and the three A/B/C MSRP rows show GripBlades' ROI when GripBlades sells direct-to-end-user at MSRP. The distributor retail-ROI matrix from ROI-Calc is deliberately omitted here because the Pre-Paid freight doesn't enter that math.
The first row of controls has three dropdowns. Customer Card lists every Customer Card you've created in ROI-Calc, in the same order — pick whoever is placing the Pre-Paid order. Sub Profile lets you pick any of the five profiles (A–E) saved under that card — useful when you keep separate profiles for "regular season," "holiday promo," "volume break," etc., and want to see how the freight adjustment lands on a specific profile's pricing. Macro Cost Basis lists every basis in ROI-Calc (Pallet, TEU, FEU, plus any custom bases you've added) — the basis you pick determines which LANDED $/PC feeds the per-pack, per-MC, and per-blade roll-ups. All three dropdowns re-read ROI-Calc every time you open the tab, so renames and newly-added cards show up without a reload.
Step 2 — Pack Size (Master Carton only)
The pack selector has exactly two options: 10 Pack MC and 40 Pack MC. Individual packs are intentionally not a choice here because Pre-Paid freight concessions only kick in for MC-level commitments; you wouldn't pay freight on a single-pack order. Picking 10 Pack MC pulls pricing from ROI-Calc's 10 Pack MC SKU row (120 packs per MC, 1,200 pcs per MC); picking 40 Pack MC pulls from the 40 Pack MC row (24 packs per MC, 960 pcs per MC). The pack-size / packs-per-MC / pcs-per-pack multipliers are the exact same ones you see on Tariff-Calc's SHIPPING REFERENCE card, so the math stays aligned across all four tabs.
Step 3 — Master Cartons Committed + EST. SHIP PRICE
Master Cartons Committed is the quantity of MCs the buyer is committing to on this Pre-Paid order. EST. SHIP PRICE is the total estimated freight (in dollars) that GripBlades will pay for that entire shipment — one number, for the whole order. You type both values directly; the math runs on every keystroke. The small Context panel on the right of the inputs echoes back your current selection in one line (Card · Profile · Basis · Pack) so you can verify at a glance that you're modelling the right scenario before trusting the numbers.
What the four results cards show
As soon as every required input is filled in, the readout below the controls builds four side-by-side result cards — one per scale of the order. Each card starts with a small red freight-allocation line that tells you exactly how the freight is spread at that scale (useful for sanity-checking: at the order-total scale it's the full EST. SHIP PRICE; at the 1-MC scale it's EST. SHIP PRICE ÷ MC qty; at the 1-pack scale it's ÷ (MC qty × packs-per-MC); at the 1-blade scale it's ÷ (MC qty × packs-per-MC × pcs-per-pack)). Below the freight-allocation line, each card shows two stacked sections:
GripBlades Margin (vs DIST. price) — four rows, one per Distributor tier (A-DIST / B-DIST / C-DIST / D-DIST). Each row prints the freight-adjusted dollar margin and percent margin, with a small red "WAS - $X.XX / X.XX% · drop -$X.XX / -X.XX%" line underneath that shows the original ROI-Calc margin and the exact hit the freight took out of it.
GripBlades ROI FROM MSRP (DIRECT-TO-END-USER) — three rows, one per MSRP tier (A-MSRP / B-MSRP / C-MSRP). Same adjusted-margin + "was / drop" drill-down as the Distributor rows. If a given MSRP tier has an active PROMO % saved on the Customer Card, a small orange PROMO X% pill appears on the row and the margin is computed against the promo-discounted effective MSRP — same formula ROI-Calc uses, just with freight subtracted.
The four scales in one sentence each
N MC ORDER (TOTAL COMMITMENT) — the whole deal: order revenue × MC qty minus landed × MC qty minus the full freight bill. Use this row to see "does this deal still make sense once we pay freight?"
1 MC (N PACK MC) — one Master Carton in isolation, with its fair share of the freight. Use this row to benchmark one MC's freight-adjusted GripBlades margin against the ROI-Calc number.
1 PACK — one pack (10 or 40 blades) inside the MC, with its per-pack freight share. Use this row to see per-pack unit economics.
1 BLADE / PC — one individual blade at the very bottom of the stack. The freight share per blade is tiny (fractions of a cent on large orders), but the "drop" line makes that tiny hit explicit so you can include it in per-unit pricing calculations.
What makes a row blank or red?
If a Customer Card profile doesn't have a price typed in for a given tier (say, no D-DIST price on Profile B), that tier's row shows "no price — —" so you know it's missing rather than computed at zero. If the freight is large enough to push the adjusted margin below zero, the percent column turns red and the dollar column shows a leading minus sign — GripBlades would be losing money at that scale once freight is paid, and the drop line makes the delta explicit.
Where the math comes from
The formulas are identical to ROI-Calc's GripBlades margin formulas, with one term added for freight. Landed per MC = LANDED $/PC × pack-size × packs-per-MC (10 MC: × 10 × 120; 40 MC: × 40 × 24). Landed per pack = LANDED $/PC × pack-size. Landed per blade = LANDED $/PC. Freight allocated to a row = total EST. SHIP PRICE ÷ the unit count at that scale. MSRP margin applies the stored PROMO % first: effective MSRP = raw MSRP × (1 − promo/100), then margin is computed against the effective MSRP. Adjusted margin $ = price − landed − freight; adjusted margin % = adjusted $ ÷ price × 100. The "was/drop" line under each row is the same two values computed without the freight term, so the drop = adjusted − original.
Where MFR SELL PRICE comes from (C8.6 fix)
Fixed in C8.6: Ship-Calc now reads the MFR Sell Price out of the base 10 Pack / 40 Pack panel in ROI-Calc (the per-pack cell you actually type into) and scales up to a per-MC value on the fly with × packs-per-MC. Before this fix, Ship-Calc was reading the sell price out of the 10 Pack MC / 40 Pack MC panel, whose sell cell is a separate storage slot that easily fell out of sync with the per-pack cell you were editing on the base panel. Symptom: the WAS - $X.XX line on the 1 MC ORDER card showed an inflated original margin because it was comparing landed cost against a stale per-MC sell number instead of your current per-pack × packs-per-MC value. Worked example: type MFR Sell Price = $5.49 on the 10 Pack panel of a customer profile whose LANDED / 10 PK MC readout is $456.84 → Ship-Calc now computes 1-MC sell as $5.49 × 120 = $658.80, original margin $658.80 − $456.84 = $201.96, and adjusted margin $201.96 − freight. Promo % values are also read off the base panel now, matching the sell price source.
How Ship-Calc stays in sync with ROI-Calc
Ship-Calc itself stores nothing — it holds no prices, promos, or LANDED $/PC of its own. Every number on the screen is read live out of ROI-Calc's roiCustomerCards, roiCardData, and roiBasisLanded stores at the moment you open or refresh the tab. That means: edit a price in ROI-Calc, switch to Ship-Calc, and the readout reflects the change; add a new Customer Card, and it appears in the dropdown; rename a profile color, and it just works. There is no "sync" button because there is nothing to sync — the data IS the ROI-Calc data.
TariffCalc is a standalone HTML file that calculates the fully-loaded landed cost of goods imported from China (or Hong Kong / Taiwan) into the United States. It was built for importers who need a fast, transparent, and up-to-date picture of their real tariff burden before making purchasing or pricing decisions.
The tool runs entirely in your browser — no server, no installation, no subscription. It works offline using cached or hardcoded fallback rates, and automatically fetches live, AI-verified rates whenever an internet connection is available.
A plain-language description of what you're importing. Used by the AI to contextualize the tariff lookup — for example, "fixed blade knives with rubber handles" helps the AI verify the correct Section 301 list assignment.
HTS / HS Code
The Harmonized Tariff Schedule (HTS) code for your product, up to 10 digits. Pre-filled with 8211.92.4060 (fixed-blade knives, rubber/plastic handles, other). You can overwrite it for any product. The HTS code is the most important input — it determines the MFN base rate, the specific per-unit rate, and which Section 301 list applies.
Country of Export / Import
Export origins: China (PRC), Hong Kong, Taiwan, Vietnam, India, United States, Canada, and Mexico. Import destinations: United States, Canada, and Mexico. Different origins may affect Section 301 applicability (Section 301 is China-specific; Hong Kong and Taiwan are not subject to it), Canadian-side China Surtax Order (2024) applicability (steel + aluminum surtax on Chinese origin, not HK/TW), and Mexican-side IEPS emergency-surcharge applicability (the Aug 2025 35% surcharge applies to non-FTA-partner origins, principally China — US and Canadian origin instead clear under USMCA / T-MEC preferential treatment).
When Country of Import = Canada, a new FX rate row appears asking for your USD→CAD exchange rate (defaults to 1.3700); this lets TariffCalc print a CAD secondary amount alongside every USD figure. The "Import Route" readout beside the FX input names the active regime in plain English — e.g. "China (PRC) → Canada — MFN + China Surtax Order (2024)" or "United States → Canada — CUSMA preferential + active counter-tariffs". The tariff engine then switches to Canadian data: MFN from the Canadian Customs Tariff T2025, CUSMA preferential 0% for US-origin goods with a Certification of Origin, Canada's 25% counter-tariffs on US steel / aluminum / non-CUSMA-compliant motor vehicles, the China Surtax Order (2024) SOR/2024-187 on Chinese steel + aluminum, 5% federal GST on the duty-paid value, and an informational CBSA row (Canada has no per-entry processing fee equivalent to US MPF). Inactive / repealed Canadian tariffs — the March 2025 broad consumer-goods counter-tariffs (removed Sep 1 2025), the original April 2025 auto surtax (superseded), the 100% China EV surtax (repealed Mar 1 2026 under the Carney-Beijing quota-for-farm-goods swap), and the proposed-but-unenacted Critical Manufacturing surtax — are shown below the active tariffs with clickable summary + full-detail popups and source links, matching the US side's Non-Active Tariffs pattern.
Provincial PST / HST are intentionally not modeled here — TariffCalc only calculates federal tariffs + 5% federal GST. Your GST-registered business can typically reclaim the 5% GST as an input tax credit (ITC) on your GST/HST return, so it's usually a cash-flow cost rather than a permanent cost. CUSMA preferential 0% only applies if you have a valid Certification of Origin on file proving the Rules of Origin are met for your HTS; without it, CBSA assesses the MFN rate instead.
When Country of Import = Mexico, a separate FX rate row appears asking for your USD→MXN exchange rate (defaults to 17.5000); this lets TariffCalc print a Mexican Peso (Mex$) secondary amount alongside every USD figure, formatted in es-MX locale. The "Import Route" readout names the active Mexican regime — e.g. "United States → Mexico — T-MEC / USMCA preferential + IVA" or "China (PRC) → Mexico — TIGIE MFN + IEPS emergency surcharge (Aug 2025) + IVA". The tariff engine switches to Mexican data sourced from SAT (Servicio de Administración Tributaria), the SHCP, the TIGIE 2025 (Tarifa de la Ley de los Impuestos Generales de Importación y de Exportación), and SIAVI: T-MEC / USMCA preferential 0% for US- and Canadian-origin goods with a valid Certificación de Origen, the TIGIE MFN ad-valorem rate for non-FTA origins, the Aug 2025 IEPS emergency surcharge of 35% on Chinese-origin consumer goods (Decreto DOF Aug 2025), the DTA (Derecho de Trámite Aduanero) of 0.176% of FOB for customs processing, and the IVA (Impuesto al Valor Agregado) of 16% applied on the duty-paid value (FOB + sum of FOB-basis duties), matching the way Canadian GST is computed. An informational PRODECON / agente-aduanal row reminds the importer that Mexican customs entries legally require a licensed agente aduanal — there is no equivalent of US "self-filer" status for commercial entries. Inactive / proposed Mexican tariffs — the proposed Sep 2025 50% Chinese-auto surtax (announced but not yet in force), and UPCI anti-dumping cuotas compensatorias on specific Chinese steel HTS lines — are shown below the active tariffs with clickable summary, legal detail, and source links, matching the US and Canadian Non-Active Tariffs pattern.
Mexican state-level taxes and ISAN (vehicle acquisition tax) are intentionally not modeled — TariffCalc calculates federal duties + DTA + IVA + any active IEPS surcharge only. IVA is generally creditable against your output IVA if your business is registered with SAT under the IVA regime, so for most importers it is a cash-flow cost rather than a permanent cost (similar to Canadian GST). T-MEC preferential 0% only applies with a valid Certificación de Origen demonstrating the Rules of Origin are satisfied for your HTS line; without it, the SAT assesses the TIGIE MFN rate instead. The Aug 2025 IEPS surcharge applies only to specific consumer-goods HTS lines listed in the DOF decree — non-listed Chinese imports clear without it but still pay TIGIE MFN + DTA + IVA.
Quantity (individual pieces)
The total number of individual pieces in the shipment — not cartons or pallets. This is critical because the MFN specific duty ($X per piece) is calculated by multiplying this number by the per-unit rate.
Cost Per Piece
Your manufacturer price per individual piece in USD. This is the declared value basis. Multiplied by quantity to get goods value; added to freight to get FOB total.
Freight & Insurance
Optional. The total cost of shipping and insurance in USD. Added to goods value to create the FOB total, which is the tariff calculation basis. If left blank, tariffs are calculated on goods value alone.
The calculation follows CBP (US Customs and Border Protection) rules exactly:
FOB Total = (Qty × Cost/pc) + Freight & Insurance
Each tariff is calculated on the FOB Total (percentage tariffs) or on the piece count (specific per-unit duties)
MPF = 0.3464% of FOB, capped at min $32.71 / max $634.62 per entry
Total Tariffs = sum of all active tariff amounts
Landed Cost = FOB Total + Total Tariffs
A math audit trail is shown below the tariff table on every calculation so you can verify each step independently. The formula is: FOB + All Tariffs = Landed Cost. Tariffs are never double-counted — they are computed once on FOB and added once to FOB.
Canadian Imports — CBSA Rules (two-pass calc)
Canadian imports use a two-pass calculation engine because GST is charged on the duty-paid value (FOB + duties) rather than on FOB alone. Pass 1 sums every basis: 'fob' tariff (MFN ad-valorem, CUSMA preferential 0%, China Surtax Order, US-origin counter-tariffs, plus any specific per-piece duty). Pass 2 then resolves basis: 'duty_paid' tariffs — currently the 5% federal GST — as rate% × (FOB + sum-of-fob-duties). Informational basis: 'flat' rows (e.g. the CBSA placeholder) read their flat_amount directly. The audit trail prints both passes explicitly, plus a USD→CAD conversion line at the bottom showing the landed total in Canadian dollars at the user's FX rate. CUSMA preferential 0% only applies if a valid Certification of Origin is on file; without one, CBSA assesses the MFN rate instead. Provincial PST / HST and recoverable input tax credits (ITCs) are intentionally not modeled — TariffCalc only computes federal duties + 5% GST.
Mexican Imports — SAT / SHCP Rules (two-pass calc)
Mexican imports use the same two-pass engine as Canada because IVA, like Canadian GST, is charged on the duty-paid value. Pass 1 sums every FOB-basis tariff: T-MEC / USMCA preferential 0% for U.S. and Canadian origin, the TIGIE 2025 MFN ad-valorem rate for non-FTA origins, the Aug 2025 IEPS Emergency Surcharge of 35% for Chinese-origin consumer goods, and the DTA (Derecho de Trámite Aduanero) of 0.176% of FOB for customs processing. Pass 2 resolves IVA at 16% of (FOB + sum-of-fob-duties). The informational PRODECON / agente-aduanal row reminds the importer that licensed agente aduanal representation is legally required for commercial entries. The audit trail labels the duty-paid line "for IVA" instead of "for GST" and prints a USD→MXN conversion line showing the landed total in Mexican Pesos using the user's FX rate. T-MEC preferential 0% only applies with a valid Certificación de Origen on file; without it, SAT assesses the TIGIE MFN rate. State-level taxes and ISAN are not modeled — only federal duties + DTA + IVA + any active IEPS surcharge.
Vietnam & India Imports (C8.6) — MFN-based
When Country of Export is Vietnam or India, neither origin has an FTA with the U.S., Canada, or Mexico, so the engine routes the call through the standard MFN ad-valorem path for the selected import country (plus any active country-of-origin overlays). The U.S. path applies HTSUS MFN + Section 122 (if still in effect) + MPF + HMF for sea freight. The Canadian path applies T2025 MFN + 5% federal GST on the duty-paid value. The Mexican path applies TIGIE 2025 MFN + DTA + 16% IVA on the duty-paid value. Section 301, the China Surtax Order (Canada), and the Aug 2025 IEPS emergency surcharge (Mexico) are all China-specific and do not apply to VN/IN-origin goods. The live AI fetcher is given the explicit origin label "Vietnam" / "India" so any new bilateral measures (e.g. anti-dumping orders on Vietnamese steel or Indian textiles) can be reflected by web search at the moment of the UPDATE TARIFFS click.
Universal invariant
Across all three import jurisdictions, the math invariant FOB + All Active Tariffs = Landed Cost always holds. Inactive / repealed / suspended tariffs are excluded from the math but rendered for transparency below the active table. Specific per-unit duties ($X/pc) are computed as rate × Qty and added to the FOB-basis sum. The basis field on every tariff record ('fob', 'duty_paid', or 'flat') determines which pass it lands in, so adding new tariff types in the future is purely a data change.
C8.6 — Vietnam & India origins. The six sub-sections below cover the most-trafficked routes through 2026. For Vietnam (VN) and India (IN) exports, no FTA applies in any of the three import countries, so the engine uses each destination’s standard MFN treatment plus any non-China-specific overlay. Concretely: U.S.-bound VN/IN goods pay HTSUS MFN ad-valorem + MFN specific (if any) + Section 122 (when active) + MPF + HMF — NO Section 301. Canada-bound VN/IN goods pay T2025 MFN + 5% GST on the duty-paid value — NO China Surtax Order. Mexico-bound VN/IN goods pay TIGIE 2025 MFN + DTA + 16% IVA on the duty-paid value — NO Aug 2025 IEPS emergency surcharge. The live AI fetcher behind UPDATE TARIFFS receives the explicit origin label “Vietnam” / “India” so any new bilateral anti-dumping or countervailing duties for specific HTS lines surface in the active table alongside the MFN base.
9.1 USA Import Tariffs (from CHINA Export)
MFN Base Duty — Ad Valorem (%)
The percentage component of the standard Congressional import tax. For HTS 8211.92.4060, this is 5.4% of FOB value. This rate is set in the Harmonized Tariff Schedule of the United States (HTSUS) and applies equally to all WTO member countries.
MFN Base Duty — Specific ($/pc)
The fixed dollar-per-piece component of the compound MFN rate. For HTS 8211.92.4060, this is $0.03 per piece. Both the ad valorem % and the specific rate apply simultaneously on the same entry — they are not alternatives.
Section 301 Tariff
A China-specific penalty tariff imposed under Section 301 of the Trade Act of 1974 after the USTR found China engages in unfair trade practices. Rate is 25% (Lists 1-3) or 7.5% (List 4A) depending on HTS classification. This is the most legally durable tariff layer — it has survived all legal challenges and has bipartisan support.
Section 122 Global Tariff
A 10% flat tariff on all imports from all countries, imposed under Section 122 of the Trade Expansion Act of 1962 (effective Feb 24, 2026) as a replacement for the IEEPA tariffs struck down by SCOTUS. This authority has a 150-day statutory limit expiring approximately July 24, 2026 unless Congress extends it.
MPF (Merchandise Processing Fee)
A CBP administrative fee for processing import paperwork: 0.3464% of FOB value, with a minimum of $32.71 and maximum of $634.62 per entry. China does not qualify for MPF exemptions (those apply to USMCA and FTA partners only).
HMF (Harbor Maintenance Fee)
A 0.125% fee on sea freight shipments, paid to the Army Corps of Engineers for harbor maintenance. Does not apply to air freight or truck/rail imports.
Non-Active Tariffs
Tariffs that are currently struck down, suspended, or inapplicable are shown in the Non-Active section below the active calculation. They are excluded from all math but shown for awareness. If any are reinstated, they will automatically appear in the active section on the next recalculation. The individual non-active tariffs currently on file are listed below for reference.
IEEPA Reciprocal Tariff — China (originally 125%)
The original 125% tariff on Chinese goods from April 2025, imposed under IEEPA (International Emergency Economic Powers Act). STRUCK DOWN by SCOTUS on Feb 20, 2026 in Learning Resources v. Trump. Replaced by the Section 122 flat 10% tariff. Zero collection since Feb 24, 2026.
IEEPA Fentanyl Tariff — China
Originally a 20% emergency tariff (reduced to 10% under the Nov 2025 US-China deal), then struck down entirely by SCOTUS on Feb 20, 2026 alongside all other IEEPA-based tariffs. Rate is 0%; collection ceased Feb 24, 2026. Congress could theoretically reimpose under a different statutory authority.
De Minimis Exemption Suspension
The $800 duty-free threshold for packages from China was suspended under 19 U.S.C. § 1321 and has been maintained through the post-SCOTUS rebuild. Does NOT affect bulk commercial entries — only small-parcel e-commerce shipments (Shein, Temu, etc.). Listed here for reference only; does not add any line item to your bulk-import calculation.
9.2 USA Import Tariffs (from MEXICO Export)
Loading U.S. tariff data for Mexican-origin imports…
9.3 Canada Import Tariffs (from CHINA Export)
Loading Canadian tariff data for Chinese-origin imports…
9.4 Canada Import Tariffs (from USA Export)
Loading Canadian tariff data for U.S.-origin imports…
9.5 Mexico Import Tariffs (from CHINA Export)
Loading Mexican tariff data for Chinese-origin imports…
9.6 Mexico Import Tariffs (from USA Export)
Loading Mexican tariff data for U.S.-origin imports…
The Update Tariffs button (top-right corner) is the only way live rates are ever fetched — it never runs automatically in the background. Click it when connected to the internet and TariffCalc calls the Anthropic Claude API with live web search to verify: current MFN rate for your HTS code, Section 301 list assignment (25% vs 7.5%), Section 122 status, and IEEPA/fentanyl tariff status.
Search for the current MFN rate for your specific HTS code
Verify which Section 301 list covers your product (25% vs 7.5%)
Confirm the current status of Section 122, IEEPA, and fentanyl tariffs
Return a structured JSON response with every tariff's rate, legal basis, and status
Include product-specific notes (known exclusions, classification nuances, watch-points)
The AI uses web search to find the most current information — not just its training data. This is why rates stay accurate even as tariff policy changes rapidly. The "Rate Last Verified" badge in the results shows the exact timestamp of the last successful live lookup.
TariffCalc is designed to work at all times, even without internet:
After a live calculation: Results are cached in your browser's localStorage keyed by HTS code. The next time you calculate the same HTS code offline, it uses the cached rates and shows a "Cached" timestamp.
No prior cache: Falls back to hardcoded baseline rates accurate as of April 2026 for HTS 8211.92.4060. A warning is shown that these are offline estimates.
Status pill: The top-right pill shows Online / Offline in real time. It updates automatically if your connection changes.
The news feed at the bottom fetches the 3 most recent US-China trade developments using the Claude AI with live web search. It is designed to always show current, relevant news — not stale cached content from months ago.
On load: Reads from a 90-day local cache. If cache is empty, fetches live news. The popup is no longer auto-fired by Update Tariffs (B3.2).
C8.6 — grid ordering: News article thumbnails are always sorted newest first by article date (with time of day when available). The 2-column grid reads left-to-right, top-to-bottom — newest article at the top-left, 2nd newest at the top-right, 3rd on the second row's left, 4th on the second row's right, and so on down the grid.
C8.6 — subject-identifier filter:Double-click any subject identifier badge (ESCALATION, NEGOTIATION, etc.) in the top-left of a thumbnail to filter the grid to only articles carrying that same identifier. The filtered view stays sorted newest-first. Double-click any identifier again (same tag or a different one) to clear the filter and go back to the default all-articles view.
Update News button: Forces a fresh live fetch and merges the new articles above the existing cache instead of replacing it (C8.6). The cache holds up to 25 articles total — when the 26th comes in, the oldest one falls off the bottom.
Green dot (C8.6): Cards that arrived in the most recent Update News batch wear a small bright-green dot in the top right corner. The dot persists until you click Update News again.
History timeline (C8.6): A horizontal track sits above the news grid showing one tick per refresh batch — newest on the left (green tick), older on the right. Click a tick to scroll the grid down to the articles from that batch. Hover any tick for the date and article count.
Source link (C8.6): Every article's deep-modal ends with a "→ View source" link to the underlying primary source (gov release, federal register notice, court filing, or news outlet). Opens in a new tab.
Offline: Falls back to rich hardcoded 2026 news articles about the SCOTUS ruling, Section 122, and supply chain shifts.
Click any card: Opens a full deep-read modal with 2-3 paragraphs of context, political background, and importer impact analysis.
Ticker: The scrolling ticker at the top of the news section shows headlines from the loaded news items. Hover to pause; click to open the full article.
The "Search HTS Codes" button next to the HTS field opens a dropdown of free, authoritative HTS classification tools. These are external resources — clicking them opens in a new tab. They are provided for quick reference so you can verify or find HTS codes without leaving the app context.
USITC Official HTS Schedule — The authoritative source. Always current.
CBP Rulings Database — Official CBP classification rulings. Search for your product to see how CBP has classified similar items.
Volza — Product-description-based search across global trade data.
Zipments — AI-assisted HTS lookup for US imports.
Zauba — Global trade database with HS code search.
Every tariff in the legal detail popup (click any tariff name, then "View full legal detail") includes a TariffCalc Source Data section at the bottom. This lists the specific URLs and official sources used to determine the rate, legal basis, and current status for that tariff.
This lets you independently verify any calculation by going directly to the source — the USITC, CBP, Congress.gov, USTR, or legal analysis from major trade law firms. If a rate ever looks off, the source links are where you start to investigate why.
When the AI performs a live lookup, it may find and use sources beyond the hardcoded list. Those are summarized in the AI Notes section below the tariff table.
Both exports are generated entirely in your browser — no upload, no server, nothing leaves your computer:
PDF: A formatted letter-size report including product info, all three summary metrics, every active tariff with rate and amount, the math formula, non-active tariffs section, and AI notes. Generated using jsPDF.
Excel: A .xlsx file with two sheets — Summary (all key figures) and Itemized Tariffs (one row per tariff, plus a non-active section). Generated using SheetJS (XLSX.js).
TariffCalc is a planning tool, not a customs filing system. Important limitations:
Rates are estimates. Always verify with a licensed customs broker before filing an entry.
The AI may occasionally misidentify a Section 301 list — especially for borderline HTS codes. The source links let you verify independently.
HMF assumes sea freight. If you're shipping by air, manually note that HMF does not apply.
Section 122 expires approximately July 24, 2026. After that date, the 10% layer may drop unless Congress acts. Always recalculate before major purchasing decisions.
This tool does not account for anti-dumping or countervailing duties (ADD/CVD), which can be significant for some product categories.
TariffCalc is not legal advice, customs advice, or financial advice.
A plain-English answer to the question "Is this file hackable or visible over the internet, and is my data safe?" GB-BizCalc is a static HTML file on your computer. As long as you open it by double-clicking locally (the browser URL bar will start with file://), it is not reachable from the internet. Nobody can type a URL and "get into" it because there is no listening server, no port, no login, no inbound network surface. The privacy posture is the same as a Word document sitting on your Desktop.
What's safe about the default (local) setup
Opened locally, the app has no outbound calls until you explicitly click Update Tariffs or refresh news. Everything you type — TariffCalc inputs, MFR-CostCalc blade costs, the MFR Cost Reference Chart, and the ROI-Calc distributor/MSRP entries — lives in your browser's localStorage, scoped to the file:// origin on your machine. Another computer cannot read that storage remotely. Shutting off your Wi-Fi does not break any core calculation; the app continues to work entirely offline using cached or hardcoded fallback rates.
Who CAN see your data
Anyone with physical or remote-login access to your computer and your browser profile — the same threat model as any file on your hard drive. If you share the .html file itself with someone, they receive the app but not your saved localStorage values. Those values live in your browser, not inside the file.
The one real caveat — when you add an API key
The moment you paste an API key and click Update Tariffs, the app starts making outbound calls from your browser to the AI provider. That is not "someone hacking in" — it is your browser reaching out. Two things to be aware of at that point:
Key storage: your API key is stored in localStorage, the same place as your supplier costs. Any JavaScript running on the same file:// origin could theoretically read that storage. Practical fix: keep GB-BizCalc in its own folder and do not open random unknown HTML files from that same folder.
Sharing the file: anyone you hand the .html file to could open it, paste their own API key, and rack up their own bill — your key is never inside the file you share. But if you email someone a version with your key still typed in, that key travels with it. Easy rule: clear the API key field before sending the file to anyone.
If you ever host it on the public web (you aren't doing this today)
That is the only scenario where "hackable via the internet" becomes a real conversation. Uploading this file to a public web server (GitHub Pages, Netlify, a shared URL, etc.) makes the UI viewable by anyone with the link. The app has no login gate, so anyone who finds the URL can use the calculator. Their typed-in numbers would live in their browser's localStorage, not yours — but the changelog, SKU names, UPCs, and any business logic baked into the file would be readable by anyone who viewed source.
Bottom line
Double-clicking a local .html file = private, offline, not reachable from the internet, not hackable in any meaningful sense. Revisit this page if/when (a) you add an API key, or (b) you put the file on a public URL. Until then, there is nothing to expose and nothing to defend.
Short answer: no, adding an API key to your local HTML app doesn't create a backdoor that lets hackers view your data or mess with your app. The key is a billing credential, not a doorway. It tells the API service "bill calls to my account" — it doesn't give anyone a way to reach back into your computer or your app.
For your specific setup, GB-BizCalc lives on your computer and runs from file://. There's no web server, no open port, nothing listening for inbound connections. The "Update Tariffs" button only makes outbound HTTPS requests when you click it — data goes out, the response comes back, and that's the entire conversation. The API service doesn't gain any ability to reach into your machine. It can see your IP address (same as any website you visit) and nothing more. Your distributor prices, profile logs, build notes, supplier costs — none of that ever leaves the browser unless you explicitly include it in an API request, which the "Update Tariffs" call doesn't do (it's pulling tariff data IN, not pushing your business data out).
The real risk with API keys is financial, not data exposure. If someone steals your key, they can make API calls billed to YOUR account until you notice and revoke it. They cannot use the key to read your localStorage, peek at your saved profiles, or modify your HTML file. The worst-case scenario is a surprise bill, not data theft from inside your app.
How would a key leak in your situation? Three realistic paths: someone with physical or remote-login access to your computer reads it out of your browser's localStorage where the app stores it; you upload the HTML file to a public place after typing the key into a field that gets serialized into the file at save time (Firefox in particular can preserve form-input values when you "Save Page As"); or malware on your machine scrapes browser storage. For a single-user local file you don't share, the practical risk is very low.
The GitHub angle is a real and well-documented problem, but primarily for developers who commit code to public repositories. Bots constantly scrape new GitHub commits looking for API keys — Anthropic, OpenAI, AWS, all of them — and start using leaked keys within minutes of the push. Most major providers have automated detection that revokes obviously-leaked keys, but you can still rack up some charges before detection fires. For you specifically, the GitHub risk only kicks in if you upload your HTML file to a public repo with the key embedded. Keep the file local and the key in browser localStorage, and GitHub leakage isn't your concern at all.
A few protective measures worth doing once you add a key: the API provider's console (Anthropic, or whoever you use) almost always lets you set monthly spend limits, so cap it at whatever makes sense for your usage and a stolen key can't drain a credit card; turn on usage alerts so you get an email if calls spike unexpectedly; use a dedicated key for GB-BizCalc rather than reusing one "main" key for everything, so if it ever does leak you only revoke and replace one thing; and rotate the key periodically as routine hygiene. If you ever do want to share the HTML file with someone, clear the API key field first (or open the file in a fresh browser profile with no localStorage saved). The localStorage value won't travel with the HTML when you share the file, but a stale value sitting in a visible input field at save time could.
How saving works. GB-BizCalc autosaves everything as you type. There is no "Save" button to click for normal work. Customer Card names, Profile (1–5) data, Build Notes, Leads, the Cost Per Click log, the API key, and the cached tariff rates all get written into a special storage area inside your browser called localStorage the moment you change them. When you close the tab and re-open the file later, the app reads everything back out and your data is just there. That's it — no files, no folders, no manual save step. The little buttons that say "Save" on individual things are just snapshots into named slots; the autosave is always running underneath.
Why your data disappears in a different browser. The catch is that localStorage is per-browser AND per-file-path. When you double-click GB-BizCalc.html in Safari, Safari makes its own private localStorage box for that file. When you double-click the same file in Chrome, Chrome makes a SEPARATE private box. Firefox makes a third. None of them can see each other's contents. They are sealed off from each other for security reasons. So if you do all your work in Safari and then open the same file in Chrome, Chrome's box is empty and the app looks like a fresh install — even though Safari still has everything saved.
The fix: three buttons in the top-right corner of the app, next to "User Guide".
Save Data ⤓.json — Click this in the browser that HAS your data. It downloads a small BackUp_GBbizCalc_MMDDYY[letter].json file to your Downloads folder (C8.6 rename — older backups saved as gb-bcalc-MM-DD-YY[letter].json continue to load correctly)
C8.6 — Lock Profile to Cost Basis
Right-click any P1 / P2 / P3 short-name button (in the CUSTOMER CARDS left-to-right scroller or a CUSTOMER CARD CLIPBOARD ghost slot) OR any PROFILE 1 / 2 / 3 long-name tab in a 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panel to see a new top-of-menu Lock to Cost Basis row with a hover-triggered submenu listing every current MACRO COST BASIS. Each entry appears as a 3-letter abbreviation — parenthesized text is stripped first, then the first three characters are uppercased. For the built-in bases that means PAL, TEU, FEU. If you add more bases they show up in the same left-to-right order that they appear in the MACRO COST BASIS bar.
Click one to lock that sub-profile to that basis for this customer card. Locked-state effects: (a) the menu row flips to Unlock from XXX on a brown background with white text; (b) both the P-button and the PROFILE 1/2/3 tab go brown-on-white; (c) the locked basis chip in the MACRO COST BASIS bar renders with a dark-brown 2px outline and very-light-brown interior, plus a small faded padlock-with-letter (P1/P2/P3) badge vertically centered at its top-left; (d) a small Locked to XXX readout in the same size and lightness as the UPC: ... label appears directly under the tab. Each customer card supports at most one lock per basis and at most one lock per sub-profile. Clicking a locked basis auto-jumps to its locked profile; clicking a locked profile auto-jumps to its locked basis. Attempting to switch to a different profile while the current cost basis is already locked to another profile is refused with a centered alert — unlock the current lock (right-click → Unlock from XXX) before you can switch.
Lock state is persisted at gbBizCalcRoiSubBasisLock_v1 in localStorage, so it survives a browser refresh. Delete a Customer Card and its locks are cleaned up automatically along with the rest of the card's data.
C8.6 refinements: the abbreviation logic strips only the parenthesis characters (keeping their content), so (TEU) 20 Ft. Cost Basis now correctly abbreviates to TEU and (FEU) 40 Ft. Cost Basis to FEU everywhere it appears (right-click menu "Unlock from XXX" row, the padlock badge letter, the confirm dialog, and the small hint under each PROFILE tab). The hint text itself was tightened from "Locked to XXX" to "Locked-XXX" (dash, no space) so it fits under the tab on a single line. Each PROFILE 1 / 2 / 3 tab reserves the hint's vertical space at render time, so a locked profile no longer shifts its button up relative to its unlocked siblings — all three tabs stay horizontally aligned. Clicking a locked profile while on a different cost basis now pops a centered YES/NO confirm reading "Go to XXX Cost Basis?". YES jumps to that cost basis and selects the locked profile (so you can edit its data there); NO leaves you exactly where you were, so the locked profile stays uneditable from the current cost basis until you decide to jump. Basis-locked brown styling now wins over the per-sid active-blue / red / purple background rules, so a locked profile shows brown-with-white-text immediately when you switch cost bases (not just after you click off + back on). The 5px has-data dot inside a brown-locked P-button renders WHITE for readable contrast.
C8.6 refinements: the C4.0 "Go to XXX Cost Basis?" confirm dialog was removed — clicking a basis-locked profile now always switches straight to it. Enforcement moved to the input layer: while the currently-active sub-profile is locked to a basis other than the currently-selected MACRO COST BASIS, every writable input on the 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC panels goes read-only (as does the MACRO COST BASIS Landed $/pc input and its Log button). Hovering a disabled input shows "Profile is Locked-XXX — switch to the XXX Cost Basis to edit." The scroller P-button now stays brown-with-white-text in every combination of active / has-data / has-custom-color, backed by a triple-class specificity bump and a defensive re-render call on every basis switch. The right-click menu row is now uppercase (LOCK TO COST BASIS / UNLOCK FROM XXX) to match the surrounding menu formatting. The invisible "Locked-XXX" placeholder that reserves the vertical space under each PROFILE tab now uses the same 10-character mono width as an actual locked label, so no cell shifts sideways when its lock state changes.
C8.6 refinements: clicking anywhere inside a basis-locked profile from a different cost basis (any 10 Pack / 10 Pack MC / 40 Pack / 40 Pack MC price field, promo button, MACRO COST BASIS Landed $/pc input, its Log button, or the click-to-edit margin percentages) now pops a centered "Locked to Cost Basis XXX — Go to XXX Cost Basis to Edit Data" alert. Clicking a HIDE button also collapses the brown-locked treatment on the MACRO COST BASIS chip, hides its padlock badge, and hides the "Locked-XXX" readout under each PROFILE tab so the app looks like it did before the Lock-to-Cost-Basis feature landed; SHOW brings all lock visuals back. The Notes-textarea toggle-then-close bug that briefly re-enabled editing on basis-locked panels is fixed (roiUpdateInputsDisabled now runs after every panel re-paint). And the brown-locked styling only paints when the P-button or PROFILE tab is the actively-selected profile — inactive locked profiles fall through to the standard has-data / empty pill styling, matching the way unlocked P-buttons behave. The always-visible padlock badge on the locked cost basis chip remains the indicator of which profile owns each basis.
C8.6 refinements: a leftover unscoped CSS rule from the original C3.9 Lock-to-Cost-Basis roll-out was causing every basis-locked P-button (and PROFILE 1 / 2 / 3 tab) to render brown even when it was not the actively-selected profile. That rule has been tightened to require the .active / .is-active class, so inactive locked profiles now render exactly like unlocked profiles do when not selected — the standard light-blue has-data pill with the darker-blue rim (or a plain white empty pill). Only the actively-selected locked profile shows brown-with-white-text. Bonus: when a locked P-button IS active, the small 5px has-data dot next to the P1 / P2 / P3 letter now paints SOLID WHITE against the brown background instead of remaining accent-blue, so it stays legible.
C8.6 refinements: the currently-selected MACRO COST BASIS chip in the top bar now renders its name in font-weight 800 (unambiguously bold) while every other chip stays at font-weight 500. Previously the selected weight was 600 — only one step above the base 500, so on a bar with two or three locked (brown-outlined) chips the visual difference was easy to miss and the last chip you locked could look bolder than the active one, mis-signaling which basis was actually being edited. The new locked-but-not-selected weight-pin also guarantees a locked chip never carries an implicit weight bump: bold now always tracks the active basis, never the lock-order.
C8.6 refinements: the BUILD NOTES and LEADS popups both now carry a two-tab header at the top — a LEFT tab defaulting to MG's Build Notes / MG's Leads, and a RIGHT tab defaulting to JJ's Build Notes / JJ's Leads. Only one tab is active at a time; the other is grayed but still visible so you can see whose slot exists. Whichever tab is active fills the full popup body so a human eye can read the most possible data at once. Double-click a tab label to rename it via a prompt dialog. The active tab is remembered per popup, so closing and reopening returns you to the tab you left. Any text you had before C8.6 is one-time-migrated into the LEFT tab so nothing is lost. Storage keys: gbBizCalcBuildNotesTabs_v1 and gbBizCalcLeadsTabs_v1. Also in C8.6: a MACRO COST BASIS chip that has a sub-profile locked to it no longer paints its light-brown fill + dark-brown outline unless the chip is the currently-selected basis. When the locked profile is not the active profile, the chip falls through to its normal styling. The always-visible faded padlock badge (P1 / P2 / P3) stays as the persistent lock indicator; only the color treatment became conditional. Combined with the C4.4 bold-when-selected rule, the active basis is now visually unambiguous even on a fully-locked bar.
containing everything (every Customer Card, every Profile, Build Notes, Leads, the cached rates, etc.). Save this file somewhere safe (Dropbox, iCloud, a USB stick, an email to yourself). It is your portable backup.
Load Data ⤒.json — Click this in the browser where you want to restore data. Pick the .json file you saved earlier. It will ask you to confirm (because this OVERWRITES whatever's currently in that browser), then it loads everything in and reloads the page. Now Chrome, Firefox, or Edge has the same data Safari did.
Save Into File ⤓.html — This is the easiest cross-browser path. It downloads a brand-new copy of GB-BizCalc.html with all your current data baked directly into the HTML file. Open that new copy in literally any browser — Safari, Chrome, Firefox, Edge — and your data will hydrate automatically the first time you open it. No JSON juggling. The downside: the file gets bigger as you add more data, and once you open it in a browser, that browser starts maintaining its own localStorage from then on (the bake-in only seeds it once). Use this when you want a portable, self-contained snapshot that "just works" anywhere.
Recommended workflow. Pick ONE browser as your "main" browser for daily work — for example, Safari. Do all your editing there so the autosave keeps everything in one place. Once a week (or whenever you've made significant changes), click Save Data and stash the JSON file somewhere with your other backups. If you ever switch browsers — say your laptop dies and you're now on a different machine, or you just want to view your data in Chrome — use Load Data to bring the JSON back, OR open a "Save Into File" copy that already has everything baked in.
What happens if you don't back up. Browsers usually keep localStorage indefinitely. But there are a handful of ways the data can vanish: clearing your browser cache or "site data" via the browser's settings menu, using "Clear browsing data" with the wrong checkboxes ticked, switching to a different user profile in the same browser, OS-level cleanup tools, browser updates that reset things (rare), or running out of disk space. None of these are common, but they all happen. A weekly "Save Data" click takes 2 seconds and protects you against all of it.
Sharing data with another person. If you want a coworker to see your saved Customer Cards, send them a "Save Into File" copy. They open it in their browser and your data is there. They can then keep building on it in their browser, and if they want to send updates back, they do the same in reverse. There is no real-time sync — everyone is working on their own private copy — but the bake-in/restore loop covers the most common collaboration pattern.
Before tonight, GB-BizCalc ran entirely in your browser — every Customer Card, cost basis, note, and setting lived in localStorage on whatever device you happened to open the file with. That is still how the app works day-to-day: the local users store (gbBizCalcUsers_v1), the audit log (gbBizCalcAuditLog_v1), and every calculator's state all live in localStorage first.
What changed tonight (Phase 2): the app now also talks to Supabase for two things — authentication (who is signed in) and, in future versions, shared state (customer cards / reference charts synced across every browser you or your team open). Phase 2 was split into small steps: C5.4 loaded the Supabase JS library and picked which project to talk to (staging vs prod); C5.5 flipped the login overlay to call signInWithPassword; C5.6 added an offline password auto-sync, a manual environment override for testing, and a "Forgot password?" link after 3 failed attempts.
Two Supabase projects, always kept in sync. There are two separate Supabase projects — gb-bizcalc-staging (agorpojlrycflemsvbgi.supabase.co) and gb-bizcalc-prod (gkzqomhvygzfkynnbybw.supabase.co). Staging is where you test everything first. Prod is the live one your team uses. Any SQL change, any auth-user change, any RLS policy tweak — do it on staging, verify, then repeat on prod.
Two custom domains, both registered on Cloudflare.gb-bizcalc-staging.dev (~$12.20/yr) and gb-bizcalc.dev (~$12.20/yr). Bought and left active on Cloudflare. Neither is deployed to yet as of C8.6 — the deploy step happens later. For now the file runs from your local machine and routes to staging by default.
The rest of the hidden guide walks each of those pieces in detail so you can recreate any of it from scratch.
Two projects on supabase.com. Log in to supabase.com/dashboard and you will see both projects listed. If you ever need to make a fresh one from scratch: click "New project", name it gb-bizcalc-staging or gb-bizcalc-prod, pick a strong DB password (SAVE IT), pick the closest region, and wait ~2 minutes for provisioning.
The anon key is safe to embed in the browser — it only grants what your RLS policies allow. Never embed the service_role key in the browser; that key can bypass RLS.
Where to find these in the dashboard. Open a project → left sidebar → Project Settings (the gear icon at the bottom) → API. Both keys and the URL are on that page. If Supabase ever rotates a key, update it here in the app in the phase-2 IIFE (search the source for SUPA_ENVS).
Key dashboard sections you will use:Authentication → Users (create/delete accounts, reset passwords), Table Editor (view rows in profiles/audit_log/app_state/tamper_events), SQL Editor (run schema migrations and one-off queries), Auth → URL Configuration (whitelist redirect URLs for the Forgot Password reset link).
Domains bought on Cloudflare.gb-bizcalc-staging.dev and gb-bizcalc.dev. Both were purchased directly through Cloudflare Registrar for ~$12.20/yr each. They show up in the Cloudflare dashboard under "Domains" and are marked Active. No DNS records were changed — Cloudflare's default nameservers are used.
Why Cloudflare specifically? Cloudflare Registrar sells .dev at wholesale (no markup, no renewal price gouge), the Pages hosting plan is free for private GitHub repos, and their nameservers give the app free DDoS/SSL/CDN out of the box. Alternatives that would also work: Netlify, Vercel, GitHub Pages.
Deploy plan (not yet done). When you are ready to deploy: (1) push the single HTML file to a private GitHub repo, (2) in Cloudflare dashboard go to Workers & Pages → Create → Pages → Connect to Git, (3) pick the repo, no build command needed (it is a static file), (4) create one Pages project for staging pointing at a staging branch and one for prod pointing at main. Attach the custom domains — gb-bizcalc-staging.dev to the staging project, gb-bizcalc.dev to the prod project. Cloudflare handles the SSL cert automatically.
Whitelisting in Supabase Auth. Once deployed, add both URLs to Authentication → URL Configuration → Redirect URLs in each Supabase project (staging URLs on the staging project, prod URLs on the prod project). Without this, the Forgot Password reset link will fail with a redirect error.
Renewal. Cloudflare Registrar does NOT mark up renewals. Watch the annual charge on the payment method you attached, or turn on auto-renew in the domain settings so the domains cannot lapse.
The app decides which Supabase project to talk to on every page load, based on the browser hostname. The logic (search source for _supaPickEnv): production credentials are only used when the hostname contains gb-bizcalc.dev AND does NOT contain staging. Everything else routes to staging.
• localhost or opening the file directly (file://) → STAGING
This is intentionally paranoid: only the exact prod domain can hit prod. Everything else — preview branches, local dev, staging domain — safely writes to the staging Supabase project.
Override (C5.6). The Cloud tab has a "Force environment" row that lets Owner override the hostname router by saving staging or prod to localStorage under gbBizCalcEnvOverride_v1. This is how you test the prod connection from your laptop before deploy.
Open Settings (⚙ icon) → CLOUD tab (Owner-only visibility). The tab shows: (1) which environment auto-routed based on hostname, (2) the full Supabase URL + a preview of the anon key, (3) three override buttons — Force STAGING, Force PROD, Use hostname (default), (4) a Test Connection button that pings auth.getSession() and returns green "Connected" or a red error.
What happens when you click Force PROD. The app calls supaClient().auth.signOut() to revoke the current session, saves 'prod' to localStorage['gbBizCalcEnvOverride_v1'], clears the local gbBizCalcAuthed_v1 flag, and reloads. On reload _supaPickEnv() sees the override and instantiates the client against the prod URL + key. The login overlay reappears; you sign in with your prod password.
Warning banners. When an override is active, the Cloud tab shows a yellow OVERRIDE ACTIVE pill next to the env badge. The header env floater (top-right, left of the gear) still shows STAGING MODE in green when overridden to staging, or nothing at all when overridden to prod (because prod is the "working normally" state and shouldn't clutter the header).
To clear the override: Cloud tab → "Use hostname (default)". That removes the localStorage key and reloads back into whichever env the hostname router picks.
Login flow ordering (C5.5). When you click Sign In, the app tries these in order: (1) call Supabase signInWithPassword — if it succeeds, load the profile row and let you in; (2) if Supabase rejects the password AND you are the Owner AND the local password matches, let you in via offline fallback; (3) if the Supabase client never loaded (offline / library failed), same fallback applies — Owner-only local check; (4) otherwise show "Incorrect password" or "Offline — non-Owner accounts need internet to sign in the first time."
Only the Owner has offline emergency access. This is deliberate — if a teammate's account is compromised and someone tries to sign in offline, they should be blocked. Only Mark can bypass the cloud check because Mark is the one physically at the machine.
Password auto-sync (C5.6). On every successful cloud login, the app quietly overwrites the local user's password field with whatever password you just typed. That way the offline fallback always accepts your current Supabase password, not some old "Welcome1" starter. This means: change your Supabase password once (via the Users tab OR the dashboard), sign in successfully once, and the local fallback is silently in sync for the next offline scenario.
Emergency reset (if you forget everything). Open browser DevTools → Console. Paste: (function(){const K='gbBizCalcUsers_v1';const s=JSON.parse(localStorage.getItem(K));const o=s.users.find(u=>u.isOwner);o.password='Welcome1';localStorage.setItem(K,JSON.stringify(s));console.log('reset');})();. Then sign in with Welcome1; offline fallback accepts it. Then use the Supabase dashboard to reset your cloud password.
Behaviour (C5.6). On each wrong password, a per-overlay-session counter (_wrongAttempts) increments. Once it hits 3, the error message grows a blue underlined "Forgot password?" link. Clicking it calls supaClient().auth.resetPasswordForEmail(email) against whatever email is typed in the Email field. Supabase then emails a magic reset link to that address.
The email template. Supabase Dashboard → Authentication → Email Templates → Reset Password. You can edit the subject line, the body copy, and the branding. Recommend adding "GB-BizCalc" to the subject so it stands out.
Where the reset link lands. The link redirects to whatever the app passes as redirectTo, which is window.location.origin + window.location.pathname. Supabase must have that URL on its allowlist (Auth → URL Configuration → Redirect URLs). For file:// mode this fails silently and the user is stranded on the Supabase reset page — a known limitation that goes away once the app is deployed to a real domain.
Counter reset. The counter zeroes on any successful login (cloud or offline fallback) and every time the login overlay is opened fresh (via Sign out).
H8. SQL Schema, RLS Policies & Seeding the Owner RowHIDDEN
The full schema is delivered alongside this build as GB-BizCalc - Supabase Schema.sql. It is idempotent — safe to re-run — and it creates four tables plus RLS policies for each.
Four tables.
profiles — 1:1 with auth.users. Carries handle (mg, jj, …), display_name, title, title_locked, is_owner, allow_alt_email, alt_email, can_assign_test_title, use_owner_login, can_manage_title_lock. Signed-in users can read all rows; only the Owner can insert/delete; each user can update their own row; Owner can update any row.
audit_log — append-only mutation log. Every localStorage.setItem write from the app pushes here. Signed-in users insert their own rows; Owner reads everything; nobody updates or deletes (missing UPDATE/DELETE policies means those ops are blocked under RLS).
app_state — a shared JSONB key-value store keyed by localStorage key. This is where Customer Card sync will land in a future version. Signed-in users read + write; only Owner can delete.
tamper_events — reserved for the Phase 3 hack-alert flow. Signed-in users insert; only Owner reads.
How to re-run. Supabase Dashboard → SQL Editor → New query → paste the entire contents of GB-BizCalc - Supabase Schema.sql → Run. Do this on both staging and prod projects. Verify by opening Table Editor and confirming all four tables appear.
Seeding the Owner. After running the schema, you must (1) create the auth user via Authentication → Users → Add user with email Mark@GripBlades.com and a memorable password + Auto Confirm ON, then (2) copy the new UUID and run this in the SQL Editor:
Repeat both steps for the prod project (different UUID, same email & insert).
Adding Jason (deferred to C8.6+). Do NOT manually create Jason's auth user in the dashboard. A future version wires the app's Add Account button to call supabase.auth.signUp and insert the profiles row atomically so provisioning stays consistent.
Blade Component Costs (Manufacturing Cost Total & Per Part)
$
$
$
$
Auto-calculated from Stamped + PVC above
Pack Size
$
$
$
$
MFR Cost Ref. Chart
LANDED Cost Ref. Chart
Inputs persist locally in this browser — each cell stamps its own Last Updated date & time on every change.
Total MFR Cost / Blade
—
Cost per Pack
—
—
Cost per MC
—
—
Cost per Pallet
—
Itemized MFR Cost Breakdown — Per Blade
US–China Trade War: Latest Developments
Live
Cost Per Click
Fetching latest US–China trade developments…
Disclaimer: TariffCalc provides estimates for planning purposes only. Tariff rates change frequently — always verify with a licensed customs broker or the USITC HTS database before filing entry. This tool is not legal or customs advice. News summaries are AI-generated.
· CALC. COSTS & MARGINS & SAVE CUST. PROFILES
CUSTOMER CARDS
No card selected
CLIPBOARD
CLIPBOARD
CUSTOMER CARD CLIPBOARD
CUSTOMER CARD CLIPBOARD
CUSTOMER CARD CLIPBOARD
Macro Cost Basis (pick scale of ordering — price drops as volume scales up, so LANDED-Cost per piece differs per basis; enter LANDED $/pc on the right of each option)
How this calculates: Margin % = (Sale Price − LANDED-Cost) ÷ Sale Price × 100. The dollar figure shown above each margin % is the per-unit gross profit (Sale Price − LANDED-Cost). Distributor rows show GripBlades' margin when selling wholesale; MSRP rows show GripBlades' ROI when selling direct to end users. Each Macro Cost Basis (Pallet / TEU / FEU) stores its own independent set of inputs — switching basis swaps all values in/out so you can compare scale tiers without losing entries.
Pre-Paid Ship-Calc
Freight-adjusted margins when GripBlades pays shipping on a Pre-Paid, large-commitment order. Pulls Customer Card + Sub Profile data from ROI-Calc.
Pick a Customer Card + Sub Profile + Pack Size, then enter MC quantity and EST. SHIP PRICE to see GripBlades' freight-adjusted margins.
How this calculates: Original margin = Price − LANDED-Cost. Freight-adjusted margin = Price − LANDED-Cost − (Freight allocated to that scale). Freight allocation: total EST. SHIP PRICE ÷ the MC qty for the "1 MC" row, ÷ (MC qty × packs-per-MC) for the "1 Pack" row, ÷ (MC qty × packs-per-MC × pcs-per-pack) for the "1 Blade" row. Pack / MC math uses the same SHIPPING REFERENCE constants as TariffCalc: 10 Pack MC = 120 packs × 10 pcs = 1,200 pcs; 40 Pack MC = 24 packs × 40 pcs = 960 pcs. MSRP rows apply the Customer Card's stored PROMO % before the margin calc; Distributor rows are not shown because manufacturer-paid freight does not affect distributor-side margins.
Settings
Sign in to GB-BizCalc
Cloud auth · checking…
Ask the Owner for your password if you forgot it. Test accounts with "Use Owner Password" on accept the Owner's password too.
Add Account
Fills in local + Supabase; can email the temp password
Account Graveyard
Deleted accounts. Revive to restore. Permanent delete leaves the auth user in Supabase — clean that up in the dashboard.