/*
Theme Name: TotalScreen
Theme URI: https://totalscreen.app
Author: ZORDERZ
Author URI: https://zorderz.com
Description: Mobile-first Field OS theme for the Screen Enclosure & Solar Panel industry. Features plugin auto-discovery with inline dashboard widgets (v2.0), 4-theme system (Light/Dark/System/Sunlight WCAG AAA), SPA architecture, role-based dashboard layouts with live KPI metrics from FreshBooks/Nutshell (v2.9.0), interactive KPI deep-links to Sales Analytics (v2.10.1), admin-editable review counts, user-facing ladybug bug reporter (v2.7.0+), fixed WP Admin User Management (v2.10.0), backend infrastructure for user goals & personal records (v2.13.0), View-As role switcher relocated to WP Admin Bar (v2.14.0), full-width opt-out for chat-style sub-views (v2.14.0), unified nav breakpoints & design-token compliance (v2.14.1), sticky header fix & WP Engine performance (v2.14.2), FOUC prevention & design-token sweep (v2.14.3), big-touch 2-col app dock, horizontal scroll lock, sales KPI data wiring (v2.14.4.3), mobile text size boost, header bleed fix, larger app tiles, deferred asset loading (v2.14.5), TS Game deferred/lazy asset integration and gameAvailable SPA flag (v2.14.6), self-hosted fonts & Lucide, app shell skeleton screen, service worker offline caching, boot error recovery (v2.15.0), PWA magic login bridge for iOS standalone auth (v2.18.0), cold-start login code fallback for PWA magic login (v2.19.0), OTP email code login for PWA (eliminates Safari dependency), KPI error states with API health diagnostics, light mode contrast improvements, Safari overflow fixes, smoother sticky app bar animation, command palette Brain Bot routing, iPad install instructions, and AI-powered workflow tools (v2.20.0). Salesperson leads action tile on the dashboard — renderLeadsTile() reads the unified /ts/v1/dashboard-items feed and surfaces "you have N leads to contact today," deep-linking into the Leads app in rep mode; fails silently and never blocks the dashboard, hidden for the shared-kiosk role (v2.21.3). Cross-app orchestrator: the dashboard "ask" field gains tsOrchestratorRoute() — a deterministic intent router that opens the Camera with a sticky pre-label in-shell, or routes contact-lookup / email-compose / document-lookup commands to the Brain Bot with a structured orchestrator_hint; adds the TS_Contact_Bridge shared contact-lookup capability ([TS_CONTACT]) with kiosk name-only disclosure and relationship/shared-job scope (v2.21.4). Inline orchestrator answers: contact-lookup commands now render a compact card directly under the dashboard "ask" field (tappable phone/email + "Open in chat →" handoff) via a new /ts/v1/contact-lookup REST endpoint that proxies TS_Contact_Bridge server-side — no chat switch, no Poe round-trip; disclosure/scope still enforced in the bridge (v2.21.5). Orchestrator camera-intent hardening: the dashboard "ask" router now tolerates common typos ("tak a photo", "snp a pic") and more phrasings ("snap a pic labeled before", "a before photo", "grab some photos tagged after") while still never firing on analytics or "take a look at…"; version bump also busts the cached app.js ?ver= so the router actually loads on devices (v2.21.6). Stronger cache-busting: theme JS/CSS (app.js, app.css, bridge.js, style.css, bug-reporter, exif-panel) are now versioned by file modification time (filemtime) appended to the theme version, so the ?ver= changes the instant a file's content changes — defeating NitroPack / CDN / PWA service-worker staleness without a manual version bump on every edit (v2.21.7). Contact-intent router widened: the inline contact card now triggers on natural phrasings the detector previously missed — bare "info"/"details" ("what's Craig Rifel's info?", "info for Craig", "Craig's details"), plus "who is X" and a lone "look up X" — while a tightened analytics guard keeps revenue/aggregate/"top N" questions out of the card (v2.21.8). Contact card data fix: the inline card was reporting "no phone or email on file" for customers who clearly had them — TS_Contact_Bridge now extracts FreshBooks fields the proven way (phone key is `mob_phone` not `mobile_phone`; email falls back through the contacts sub-array / username / pref_email; address tries shipping s_* fields), requests include[]=contacts on the client lookup, and salvages a phone-looking tail appended to the street string (FreshBooks stores some numbers that way, e.g. "23762 Moonglow Court 248-635-7783") — so Craig Rifel's phone + email now render in the card (v2.21.9). Contact-intent detector made punctuation/typo-tolerant: a normalize-then-match fallback strips stray apostrophes/semicolons/slashes and fires the inline card when an info-word ("info/details/contact/number/phone/email/reach") and a plausible name both appear in any order — so messy real-world typing like "what';s craig rifles info/" or "craig rifel info" still resolves, while analytics/"top N"/"list all" stay excluded (v2.21.10). Server-authoritative orchestrator: the dashboard "ask" field now resolves read verbs through a new Poe-free GET /ts/v1/orchestrate endpoint (TS_Orchestrator) that classifies intent server-side and returns contact/document-lookup data from the capability bridges in one call — so phrasing robustness no longer depends on the JS regex (which remains as an instant row-label hint + offline fallback). Email-compose is correctly disambiguated from contact lookup (checked first) and opens the full chat; contact + document lookups render inline with an "Open in chat →" handoff (v2.22.0). Contact card data fix (round 2): TS_Contact_Bridge now resolves FreshBooks clients with the SAME query the chat path uses — search[user_like] (broad fuzzy across all name fields, returns FULL client objects with phone/email/contacts) + include[]=contacts + archived-pool retry — instead of the leaner search[lname], which was why the inline card showed "no phone or email on file" and missed typo'd surnames. Added safe typo-tolerant disambiguation (prefix / Levenshtein ≤2) so "craig rifles"/"craig rifle" resolve to Craig Rifel without guessing between different people (v2.22.1). Inline ask field (no pop-up): the dashboard "Ask a question…" bar is now a real text input you type into directly, with results appearing in a dropdown beneath it — the separate command-palette overlay/dialog is removed. Empty field shows just the bar; typing opens the dropdown (apps, the inline contact/lookup card); Escape/outside-click/clear closes it; Ctrl/⌘-K focuses it. autocorrect/autocapitalize/spellcheck stay off so names aren't altered (v2.23.0). Self-healing app shell: the service worker is now actually served in production — WP Engine's nginx 404s the old virtual /sw.js route (any .js path with no physical file), so the SW had silently never registered on any device since v2.18.0; it is now served and registered at the extension-less /ts-sw. The SW (2.22.0) fetches same-origin GET navigations network-first with an HTTP-cache bypass (offline falls back to cache), and logged-in front-end page loads send explicit no-cache headers — so installed PWAs can never again pin a days-old dashboard shell with stale nonces and stale ?ver= script URLs (the root cause of the June 2026 camera "nonce check FAILED" saga). Also ships the previously-undeployed sw.js camera-drain hardening: single-uploader lease (2.21.1), ArrayBuffer-record support (2.21.2), X-WP-Nonce auth (2.21.3), and empty/size-mismatch byte refusal (2.21.4) (v2.23.1). Mobile dashboard density + unification pass (field feedback): the greeting + app dock are compacted from ~25% of the viewport toward ~15% while every tap target stays >=44px (shorter dock tiles, smaller icon plate, single-line greeting); a unified dashboard widget shell gives every app's widget the same card frame, footer rhythm and max-height so surveys and estimates no longer display at wildly different sizes (the estimate widget is capped to match the rest); survey batch text gets a font-size floor and unclamped names (no more crammed/truncated batch info); the light-mode page background is nudged one cooler step for clearer card separation; theme-aware contrast + width guards are added for embedded widgets (Commission-calculator dark-on-dark text is pinned to the active theme token and inner content can no longer exceed the card width); and the Knowledge Base uses the full horizontal width in the full-screen viewport via a new per-app data-app-id tag on the viewport/body. The ask field gets a clearer placeholder ("Search apps or ask a question") and explicit top-margin separation from the refresh button, and a camera-capture command ("take a picture") now ALWAYS opens the real camera in-shell — with or without a pre-label — instead of falling through to the Brain Bot, firing a launch haptic on open (v2.24.0). Screenshot follow-up: the dashboard KPI metric cards (.kpi-card in .kpi-grid — "Website Reviews", "FreshBooks revenue", "Contacted", "New Leads", "Leads→Jobs") are normalized to ONE uniform size via grid-auto-rows:1fr + a single min-height, with the larger "primary" revenue cards pinned to the same footprint on phones (no 2-col span, matched value size, single-line sub) so the grid reads as even squares; the .stat-pill strip is equalized the same way; and the top zone is trimmed further (tighter dash-top/greeting/search padding, smaller app-dock and compact sticky-strip icon plates) to get the header under ~15% while keeping tap targets >=44px (v2.24.1). Stale-PWA-shell hardening: the service worker (now 2.24.2) deletes every CacheStorage bucket except the auth/camera bridge cache on activation and accepts a skip-waiting message; the registration script checks for an updated SW on each load, tells a freshly-installed worker to take over immediately, and reloads once — so a deployed update can no longer keep serving an old cached shell/CSS on an installed PWA; plus a tiny, subtle build stamp ("TotalScreen 2.24.2") at the bottom of the dashboard so the running build is verifiable at a glance (v2.24.2). App-tile label alignment fix: long app names (Commission, Knowledge, Analytics, Messages) were tagged .is-long and shrunk, but the same rule also collapsed the label's reserved height from a two-line band to one line — which dropped those labels to a lower vertical position inside the centered tile, so a row read as mismatched (short names larger and lower, long names smaller and higher, no shared baseline). The .is-long rule now shrinks the font ONLY; the dock-tile label keeps a fixed top-anchored two-line band (sized off a stable line-height unit) and the springboard-grid label reserves a fixed single-line band, so every app label in a row sits on the same baseline regardless of name length (v2.24.3). Dashboard size restore: the v2.24.0 mobile-density pass shrank the app dock icons (72→56 / 84→64px) and tiles (130/150→96/118px) with UNSCOPED !important rules, so the shrink applied at every width — making the icons and tiles look too small on tablet/desktop. Those overrides now RESTORE the original larger footprint at all widths (icon 72px / 84px ≥600px, tile min-height 130 / 150px), the conflicting v2.24.1 .app-dock padding re-trim is removed, and the unified dashboard widget cap is changed from a flat 46vh (which letterboxed widgets to ~290px on tall windows) to a responsive min(620px, max(380px, 52vh)) so each widget portal is taller and better-proportioned on desktop while phones still show ~two cards and the unified shell + internal scroll are unchanged. The greeting trim, KPI-card uniformity, and the scrolled compact sticky strip remain compact by design (v2.24.4). Uniform app-label type + larger icons: per field request, every app label under a tile now renders at ONE uniform size — the .is-long shrink (which made long names like Estimates/Commission/Knowledge/Analytics ~30% smaller than short names in the same row) is removed; long names instead wrap to two lines inside the existing top-anchored reserved band, so labels are the same size, never truncated, and still share a baseline. App icons are enlarged at all widths: dock icon 72→80px (phone) / 84→96px (≥600px) with proportionally larger glyphs, and the springboard-grid icon 72→80px; the tablet/desktop dock tile grows 150→158px to keep the larger plate comfortable (v2.24.5). Dashboard widgets now fill the viewport when their content needs it: the unified widget cap was a flat ~380px that clamped every widget to a short slab (the Game's content is ~678px but was squeezed), so the cap is now a generous content-driven ceiling (max-height max(560px, 88vh)) — a tall widget like Game/Camera expands toward the full screen while short widgets stay short (max-height only caps, never stretches) and the inner body still scrolls past the ceiling. Also fixed the dead Game container-budget rule: it targeted data-app-id="ts-game" but the registered id is "game", so the budget never applied and the game stayed tiny — the selector now matches "game" and gives the game body a min-height so it fills the taller widget (budget 480/720/78vh). Also fixes unreliable app-icon → widget navigation: tapping a tile often did nothing because the launch used scrollIntoView({behavior:'smooth'}) on the #sv-dash overflow container, where a programmatic smooth scroll is silently dropped — bridge.js now scrolls the container deterministically (computed offset from the live sticky-chrome height, guaranteed arrival via direct scrollTop on the next frame, rAF timing instead of a fixed 150ms timer); tested across all 15 inline-widget apps, every one lands its widget just below the chrome (v2.24.6). Game black-screen fix: the Game widget collapsed to a ~44px black sliver (its 678px content overflowed a 44px body, clipped to nothing — the "black screen" report). The v2.24.6 attempt put min-height on the body, which does nothing because the body is a flex item in a max-height-only flex column; v2.24.7 instead gives the CONTAINER a min-height (max-height alone never stretches it) and makes the body flex:1 1 auto with a matching min-height so it fills the grown container — the canvas now paints at full height instead of a sliver (verified by flex-layout modeling against the corrected rules) (v2.24.7). Game black-screen — ACTUAL root cause + cache-buster repair: v2.24.7's CSS was present on the live page yet the game stayed a sliver because the budget variable resolved to 0px at point of use — the ts-game PLUGIN's game.css (v1.5.2) declares :root{--ts-game-max-h:0px} and loads AFTER the theme's app.css, so at equal :root specificity the plugin's 0px won; the theme's container min-height and the body's max-height then both computed to 0 and clipped the 324px game to ~44px. Fixed by re-declaring the budget (480/720/78vh) on the higher-specificity .dash-widget-container[data-app-id="game"] selector, which beats :root regardless of load order (proven live: the container then computed the real value). SEPARATELY, every theme asset was shipping as ?ver=.1781587569 (leading dot, no version) and bare-$version assets as ?ver=7.0 (WP-core fallback) — i.e. wp_get_theme()->get('Version') was returning EMPTY in the enqueue callback, which both malformed the cache-buster and, with the assets' 1-year max-age, froze every browser onto one cache key that never changed, so deployed fixes could never reach an already-cached visitor (the real reason the dashboard looked broken / icons missing on cached sessions while a fresh load was fine). Hardened functions.php to pin an explicit 2.24.8 version floor and never fall back to empty (v2.24.8). Desktop dashboard correctness pass (field review): (1) UNIFORM app-tile labels — every springboard/dock icon name now renders at ONE size token (--ts-applabel-size, per-breakpoint) sized so the longest name (Commission/Knowledge) fits on a single line; the prior 2-line band + .is-long no-op that let labels drift in size/position is overridden source-last, so a row reads as one consistent size on a shared baseline. (2) NO INTERNAL WIDGET SCROLL — the v2.24.6 unified shell capped every container at max-height:max(560px,88vh) with overflow-y:auto, trapping a scrollbar inside each widget; that cap is removed (max-height:none) and the body returns to overflow:clip, so widgets grow to their natural content height and the dashboard PAGE scrolls as one document (this also re-satisfies WIDGET-OVERFLOW-CONTRACT-v1, which overflow:auto had violated). Long sections like Leads "All Leads" open via in-flow expansion panels that grow the widget downward instead of scrolling inside it. (3) TILE-TAP NAVIGATION FIX — the launch scroll used widget.offsetTop, which is measured from the nearest positioned ancestor (offsetParent), not the scroller; once widget containers gained position:relative for sticky containment, offsetTop collapsed and taps landed near the top of the dashboard instead of on the tapped widget. bridge.js now computes the scroll target from getBoundingClientRect() deltas (offsetParent-independent), so every tile reliably lands its widget just below the chrome. The content-sizing change also eliminates the empty dark surface a force-grown short widget used to show (the "black screen"). (4) SURVEYS BATCH-HISTORY BANNER — the section header was position:sticky pinned to the PAGE chrome, so scrolling past the Surveys widget left it floating over other widgets; sticky was only correct while the widget body scrolled internally (which fix 2 removed), so it now binds to the page and escapes — the header is therefore made a normal in-flow header (position:static) that scrolls with its own content and can never hover over unrelated widgets (the surveys plugin also lifts its lead/approval-list internal scroll caps). (5) COMMISSION + SCHEDULER DARK TEXT — the dark/system contrast guard is broadened to every text element AND the form controls (date/dropdown inputs) inside both the Commission calculator and the TS Scheduler widgets, pinning text to --sys-text and control surfaces to --sys-surface-raised so nothing renders dark-on-dark. Companion plugin updates: ts-satisfaction-surveys v2.9.7 (in-flow batch-history header) and ts-sales-leads v2.5.1 (no nested scroll at any width, not just mobile) (v2.25.0). App-label size made a single FLAT value at every breakpoint: per field request the uniform app-tile label is now a hard 18px on ALL widths (was graduated 16→20px), with text-overflow:ellipsis trimming "Commission" on the narrowest 3-col phones rather than shrinking or wrapping — so every label is the exact same size on every device (v2.25.1). Black-screen-until-scroll fix: tapping any app icon flashed a totally black screen until the user scrolled, then the widget appeared. Cause was the view fade — .sub-view.active{animation:viewFadeIn} plays a 250ms opacity 0→1 fade whenever .active is (re)added, and because the app dock, the springboard "Apps" grid AND the inline widgets all live in the SAME #sv-dash sub-view, every icon tap re-ran switchView('sv-dash') on the already-active view, restarting that fade from opacity:0 (the black frame); a separately-dropped programmatic smooth scroll then left the user in empty space (near-black page bg) until a manual scroll forced a repaint. Fixed: switchView() no longer churns .active / restarts the fade / resets scroll on a same-view re-entry, and bridge.js now lands the launch scroll INSTANTLY (not smooth), waits for the widget to actually lay out before measuring, and verifies arrival across a few frames — so the tapped widget is on screen and painted with no black flash (v2.25.2). Shared-FreshBooks-client hardening (interop): TS_Core_FreshBooks::api_request() now joins base+endpoint with exactly one slash and passes absolute URLs through, permanently closing the legacy "https://api.freshbooks.comaccounting/..." concatenation bug that several plugins (TS Ember Cutter, TS Ember Receipt) still cite as the reason they ship private FreshBooks clients on the same single-use OAuth tokens (a token-revocation hazard, contract §1.2) — those plugins can now safely delegate to the shared client. No behavior change for existing callers (get_invoices/get_clients/get_estimates already passed a leading-slash endpoint); this only makes the join regression-proof (v2.25.3). Log-noise fix: a targeted `doing_it_wrong_trigger_error` filter now suppresses ONLY the WordPress 6.7 "_load_textdomain_just_in_time … woocommerce domain triggered too early" notice (WooCommerce-core registers its textdomain strings before `init`, firing this benign notice on every request and flooding debug.log with ~3,700 identical entries/day); every other _doing_it_wrong notice still logs, so no real signal is lost (v2.25.4). Accessibility: the Settings → Field Preferences collapsible header is now a proper button — role="button", tabindex, aria-controls, and an aria-expanded state that flips on open/close, plus Enter/Space keyboard toggling (it was a click-only div with no ARIA state, invisible to screen readers and unreachable by keyboard) (v2.26.1). Cache-buster correctness: the hardcoded theme-version floor in functions.php (used when wp_get_theme()->get('Version') returns empty on WP Engine) was still 2.25.4, so ?ver= read 2.25.4 even after the 2.26.x deploys; bumped to keep lock-step with style.css (v2.26.2). Analytics no longer a top-bar icon — Brain Bot stays reachable: the command-palette / search "Ask Brain Bot" primary row guarded its visibility on getVisibleApps(), which excludes secondary surfaces; now that the Analytics app is registered with springboard:false (so it stops appearing as a separate top-bar "Analytics" icon and is reached only via the permanent bottom "Chat" tab — companion ts-sales-analytics v1.20.10), that guard would have silently dropped the Brain Bot row. The guard now keys on the REGISTERED apps (tsData.apps) instead — i.e. "is Analytics active?" — so Brain Bot remains available from search while the icon is gone. The bottom Chat tab (setupDedicatedPage, keyed on pageMode) and every openApp('sales-analytics') deep-link (digest, camera orchestrator) are unaffected because they resolve against registered apps, not visible ones (v2.26.3). Dashboard "ask" field — more app launching: the orchestrator (tsOrchestratorRoute) gains a generic, data-driven app-launch detector (tsDetectAppLaunch) so natural commands open any app's dashboard widget instantly in-shell, like the Camera already did — "open the schedule" (optionally for a day), "new estimate for the Smith job", "start a sketch", "check stock for sliders", "pull up commission for last month", "open leads/surveys/prep/messages/knowledge/receipts/game". Each launch is a kind:'shell' action that calls openApp(id, {source:'orchestrator', …context}) → jumps to the inline widget (no Brain Bot round-trip); context like a customer/job name or a date/period is captured and passed through so the widget can pre-focus. Precedence is careful: the new launches run AFTER the Camera block and only fire on an explicit launch verb (open/new/start/go to/check/…) or a query that starts with the app word — so analytic questions ("how many estimates this month") and inline lookups ("estimate for Steve Rifel", "Craig's info") still fall through unchanged to the document/contact cards and the Brain Bot. Apps not installed/visible are skipped. Client-side only; the server /orchestrate read-verb path and all existing intents are untouched (v2.27.0). Dashboard "ask" field — inline COMMISSION answer card with drill-down: a command naming a salesperson ("commission for Aaron, May 2026", "Aaron's commission for May") now renders a fast inline card under the field — headline figure first, with collapsible drill-down (jobs counted, how it's calculated, by-product-line split) — instead of opening the whole widget. Both the client router (tsDetectCommissionInline) and the server (TS_Orchestrator::detect_commission → commission_calc_for_tsa) require a PERSON, so true aggregates ("total commissions this month", "commission by rep") still go to chat and a bare "open commission" still launches the widget. The figure comes from the existing tier-gated TSCC_TSA_Bridge (companion ts-commission-calculator v2.8.5 adds the breakdown payload) — read-only, HARD-FORBIDDEN on the shared kiosk. The card offers "Open in Commission ↗" (pre-fills salesperson + period) and "Ask in chat ↗". Reusable template for further inline answer cards (v2.28.0). Commission-router robustness pass (field stress-test): the named-person detector now (a) handles verb phrasings — "what did Aaron make in commission", "how much commission did Frank earn" — via a "did <Name> earn/make/get" pattern; (b) no longer mis-captures verbs/adjectives as the salesperson ("make"/"average"/"earn" are rejected by an expanded stop-word list); (c) routes AGGREGATE commission questions to the Brain Bot, not the widget — "commission by salesperson", "commission report for the team", "average commission", "how much did we pay everyone", "commission rates", "how do commissions work" all correctly go to chat (the launch detector now skips the Commission app on aggregate framing); while bare "open commission" still launches and "<Name>'s commission" still shows the inline card. The server detector (TS_Orchestrator::detect_commission) is synced to the same logic so the authoritative path matches the client (v2.28.1). Inline-card SMART AUTO-EXPAND: cards still lead with the simplest, most-accurate headline figure and keep every drill-down box COLLAPSED by default — but when the request explicitly asks about a section, that one box auto-opens. "Aaron's commission by product" opens the product split; "which jobs did Aaron get commission on" opens the jobs list; "how is Aaron's commission calculated" / "what's the rate on Aaron's commission" opens the rate basis; a plain "Aaron's commission" opens nothing. A named-person drill-down like "Aaron's commission by product" now correctly renders the card (it was previously rejected as an aggregate); true cross-person aggregates ("commission by salesperson", "average commission") still go to chat. The focus hint is computed on both client and server (kept in sync) and forwarded so the right box opens (v2.28.2). Card-detection robustness round 2 (live-test fixes): a period word sitting BETWEEN the salesperson and "commission" no longer breaks detection — "Aaron's May commission", "which jobs make up Aaron's May commission", and "how is Aaron's May commission calculated" all render the card with the correct auto-expanded box (jobs / basis); and "what's the rate on Aaron's commission" no longer leaks "on" into the captured name. Added an optional intervening-period-word allowance to every name pattern plus leading-preposition stripping, synced across the client (tsDetectCommissionInline) and server (TS_Orchestrator::detect_commission) (v2.28.3). Server/client sync fix: "how is Aaron's May commission calculated" was matching on the SERVER but capturing "is Aaron" — the server's leading-strip list lacked "is"/verb fillers the client already stripped, so the server returned chat and the inline card never rendered. The server $take() leading-strip now mirrors the client takeName() LEAD list exactly (v2.28.4). Name-capture robustness round 3 (live test): "break down Derek's commission by product" no longer captures "down" into the name (subject was empty / "down Derek") — "break/breakdown/down/drill/into" added to the leading-strip list on both client and server (v2.28.5). Mobile commission-card detection fixes round 4 (field stress-test), client+server in lock-step: (1) a personal-earnings question with NO "commission" word now resolves the card on this sales dashboard — "how much did Frank earn last month", "what did Diana make", "how much does Aaron take home" — via a tightly-scoped did/does <Name> <earn-verb> | <Name> earned/made/took-home frame that never fires on generic "how do I earn rewards"; (2) a possessive-name query like "Frank's commission rate" / "what's Diana's May commission" is treated as person-specific and no longer swept into the company-wide rollup skip (which had caught "commission rate(s)"/"total commissions"), so it renders the card instead of going to chat; (3) "which jobs count toward Diana's May commission" captures "Diana" not "toward Diana" ("toward/towards/count" added to the leading-strip + drill-down preposition list); (4) an orphaned leading "s " left when "what's"/"that's" backtracks the possessive regex is stripped in takeName()/$take() so "what's Frank's commission rate basis" captures "Frank" not "s Frank"; and the companion bridge (ts-commission-calculator v2.8.6) strips a trailing possessive before the salesperson-code check so "DM's commission" resolves the DM code (the apostrophe-s previously defeated the code match). Verified with a 10-input regex regression trace, all pass (v2.28.6). Front-end chat robustness round 5 (six enhancements from a stress-test sweep), client+server in lock-step: (1) QUARTER & YEAR periods now resolve — "Frank\'s Q2 commission", "first quarter", "this/last quarter", "QTD", "this year", "YTD", "last year" and a bare 4-digit year are parsed to the correct date window in the bridge resolve_period() (previously all silently fell back to the current month); (2) first-person EARNINGS resolve to self — "how much did I earn this month", "what have I made" now show the current user\'s card instead of trying to read "I"/"have I" as a salesperson; (3) FUZZY name matching (typo tolerance) in resolve_subject — "frnak"→Frank, "geof"→Geoff, "kathy"→Kathie via a unique-only Levenshtein<=2 first-name match, plus an explicit dictation-alias map for reliable mis-hearings ("Jeff"→Geoff since there is no Jeff, "cathy"→Kathie, etc.); (4) MULTI-PERSON queries ("Frank and Diana commission") capture the first valid salesperson and show that card (with the row label noting others were named) instead of failing on "and Diana"; (5) app-launch verbs tolerate dictation/typing slips ("opne/oepn/luanch/shwo/chekc") via a tight Damerau-Levenshtein<=1 check that never fires on analytic questions; (6) document lookup honours an explicit doc-type — "estimate for X" / "invoices for X" filters the returned documents to that doc_type instead of dumping every document. Companion bridge ts-commission-calculator v2.8.7 (resolve_period quarters/years, fuzzy + alias name matching). Verified with syntax checks and logic-trace suites for every gap (v2.28.7). Inline-box SPEED pass for the homepage chat field (employees\' primary interface, so it must feel instant): (1) SERVER-SIDE CACHING — commission figures (TSCC bridge v2.8.8) and contact lookups are cached in 10-minute WordPress transients keyed per subject/customer + concrete date-window + requesting user, so the same rep/customer looked up repeatedly returns in ~50ms instead of a 2-35s live FreshBooks+LLM pass, and cached hits don\'t depend on a live FreshBooks token; the commission key is per subject_uid so a cached figure can never cross a tier/permission boundary, and QTD/YTD windows self-expire across day boundaries because \'today\' is part of the key; (2) INSTANT SKELETON — the inline card frame (subject + period, or contact name) paints immediately from the client-side detection with a shimmering placeholder for the figure, then fills when the server responds, so the box never shows a blank \'looking that up\' pause; (3) FRESHNESS — a cached commission card shows a subtle \'Updated HH:MM\' stamp so the figure is trusted. Reduced-motion users get no shimmer animation. Companion ts-commission-calculator v2.8.8 (commission result cache) (v2.28.8). Compound/complex request robustness for the homepage chat box (client+server): when a single ask bundles two intents or a comparison, the commission name-capture no longer leaks a connector or the word \'commission\' into the salesperson name. \'and/or/vs/versus\' and \'commission/commissions\' are added to the leading-strip and stop-word lists, and the multi-person pattern accepts vs/versus/or as separators \u2014 so \'Craig\'s phone and Frank\'s commission\' \u2192 Frank, \'Frank vs Diana commission\' \u2192 Frank, and \'how much commission Frank brought in during May\' \u2192 Frank (previously these captured \'and Frank\'/\'vs Diana\'/\'commission Frank\' and failed). The first clearly-named person wins and renders their card; verified against a 12-case regression suite incl. all prior gaps and the company-wide chat guards (v2.28.9). App-launch quick-look improvements (client): (1) the Game now launches on the natural verb \'play\' \u2014 \'play a game\', \'let\'s play a game\' \u2014 not just \'open/start the game\' (\'play\' added to the launch-verb list only, so it never causes typo over-fires and \'display\'/\'playback\' are unaffected); (2) the estimate-launch customer capture strips a leading article/possessive so \'new estimate for the Smith job\' pre-fills \'Smith\' (not \'the Smith\'), \'for a Johnson estimate\' \u2192 \'Johnson\', \'for my Wilson job\' \u2192 \'Wilson\'. The deliberate \'estimate for <name>\' \u2192 inline document-lookup boundary (no launch verb) is unchanged (v2.28.10).
Version: 2.29.0
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 8.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: totalscreen
Tags: mobile-first, one-column, custom-colors, custom-logo, full-width-template, theme-options
*/

/* ================================================================
   TOTALSCREEN TOOLS V6 — DESIGN SYSTEM
   3-Tier Token Architecture: Primitive → Semantic → Component
   ================================================================ */

/* ---- TIER 1: Primitive Tokens ---- */
:root {
  --ref-brand-50:#EBF5FF;--ref-brand-100:#D6EBFF;--ref-brand-200:#ADCFFF;
  --ref-brand-300:#7AB3FF;--ref-brand-400:#4796F7;--ref-brand-500:#2C5F8A;
  --ref-brand-600:#1E3A5F;--ref-brand-700:#162D4A;--ref-brand-800:#0F1F35;
  --ref-brand-900:#091526;--ref-brand-950:#050D18;
  --ref-gray-0:#FFFFFF;--ref-gray-50:#F8FAFC;--ref-gray-100:#F1F5F9;
  --ref-gray-200:#E2E8F0;--ref-gray-300:#CBD5E1;--ref-gray-400:#94A3B8;
  --ref-gray-500:#64748B;--ref-gray-600:#475569;--ref-gray-700:#334155;
  --ref-gray-800:#1E293B;--ref-gray-900:#0F172A;--ref-gray-950:#020617;
  --ref-green-500:#10B981;--ref-green-600:#059669;
  --ref-amber-500:#F59E0B;--ref-amber-600:#D97706;
  --ref-red-500:#EF4444;--ref-red-600:#DC2626;
  --ref-space-1:4px;--ref-space-2:8px;--ref-space-3:12px;--ref-space-4:16px;
  --ref-space-5:20px;--ref-space-6:24px;--ref-space-8:32px;--ref-space-10:40px;
  --ref-space-12:48px;--ref-space-16:64px;--ref-space-20:80px;
  --ref-radius-xs:4px;--ref-radius-sm:8px;--ref-radius-md:12px;
  --ref-radius-lg:16px;--ref-radius-xl:20px;--ref-radius-2xl:28px;--ref-radius-full:9999px;
  --ref-font-xs:16px;--ref-font-sm:18px;--ref-font-base:20px;--ref-font-md:22px;
  --ref-font-lg:25px;--ref-font-xl:30px;--ref-font-2xl:34px;--ref-font-3xl:42px;
  --ref-dur-fast:150ms;--ref-dur-norm:250ms;--ref-dur-slow:350ms;
  --ref-ease:cubic-bezier(.4,0,.2,1);--ref-ease-out:cubic-bezier(0,0,.2,1);
  --ref-ease-spring:cubic-bezier(.32,.72,0,1);
  /* Category Colors (v2.14.1) */
  --cat-sales:#7C3AED;--cat-finance:#059669;--cat-service:#2563EB;
  --cat-field:#EA580C;--cat-admin:#DC2626;
  --cat-ops:#0891B2;--cat-team:#DB2777;
}

/* ---- TIER 2: Semantic Tokens — LIGHT (default) ---- */
:root, :root[data-theme="light"] {
  --sys-bg:var(--ref-gray-100);--sys-bg-alt:var(--ref-gray-200);
  --sys-surface:var(--ref-gray-0);--sys-surface-raised:#F8FAFC;
  /* v2.14.0: surface-raised is now slate-50 (one step from base white surface)
     so plugins relying on a real one-step elevation — alternating rows,
     skeleton shimmer gradients, card-on-card patterns — render the same in
     light mode as they already do in dark/system. Pre-2.14 this resolved
     to var(--ref-gray-0) (=#FFFFFF), giving zero contrast against surface. */
  --sys-surface-blur:rgba(255,255,255,.82);
  --sys-surface-subtle:rgba(0,0,0,.03);
  --sys-text:var(--ref-gray-900);--sys-text-sec:var(--ref-gray-600);
  /* v2.21.0 contrast retune: ter was gray-500, which passes on white surface
     (4.76:1) but FAILS on the --sys-bg gray-100 header zone (4.34:1). gray-600
     clears AA on both surface (7.58:1) and bg-100 (6.92:1). */
  --sys-text-ter:var(--ref-gray-600);--sys-text-inv:var(--ref-gray-0);
  --sys-text-brand:var(--ref-gray-0);
  --sys-border:var(--ref-gray-200);--sys-border-strong:var(--ref-gray-300);
  --sys-brand:var(--ref-brand-500);--sys-brand-light:var(--ref-brand-50);
  --sys-brand-hover:var(--ref-brand-600);--sys-brand-dark:var(--ref-brand-700);
  --sys-success:var(--ref-green-500);--sys-warning:var(--ref-amber-500);--sys-error:var(--ref-red-500);
  --sys-overlay:rgba(0,0,0,.45);
  --sys-shadow-sm:0 1px 3px rgba(0,0,0,.08);
  --sys-shadow-md:0 4px 12px rgba(0,0,0,.1);
  --sys-shadow-lg:0 12px 32px rgba(0,0,0,.12);
  --sys-shadow-xl:0 24px 48px rgba(0,0,0,.16);
  --sys-icon-wt:1.75;--sys-font-wt:400;--sys-font-wt-md:500;
  --sys-font-wt-sb:600;--sys-font-wt-b:700;
  --sys-focus:0 0 0 3px var(--ref-brand-200);
  --sys-topbar-bg:var(--ref-brand-700);--sys-topbar-text:var(--ref-gray-0);
}

/* ---- DARK ---- */
:root[data-theme="dark"] {
  --sys-bg:var(--ref-gray-950);--sys-bg-alt:var(--ref-gray-900);
  --sys-surface:var(--ref-gray-800);--sys-surface-raised:var(--ref-gray-700);
  --sys-surface-blur:rgba(30,41,59,.82);
  --sys-surface-subtle:rgba(255,255,255,.05);
  /* v2.21.0 contrast retune: secondary/tertiary text must clear WCAG AA on the
     WIDGET SURFACE (--sys-surface = gray-800), not just the page bg. Old values
     (sec=gray-400 @5.71:1 borderline, ter=gray-500 @3.07:1 FAIL on surface)
     produced the "gray text on dark-gray" complaint. New: sec=gray-300 (9.85:1,
     AAA) and ter=gray-400 (5.71:1, AA) on gray-800. */
  --sys-text:var(--ref-gray-100);--sys-text-sec:var(--ref-gray-300);
  --sys-text-ter:var(--ref-gray-400);--sys-text-inv:var(--ref-gray-900);
  --sys-text-brand:var(--ref-gray-0);
  --sys-border:var(--ref-gray-700);--sys-border-strong:var(--ref-gray-600);
  --sys-brand:var(--ref-brand-400);--sys-brand-light:var(--ref-brand-900);
  --sys-brand-hover:var(--ref-brand-300);--sys-brand-dark:var(--ref-brand-200);
  --sys-success:var(--ref-green-500);--sys-warning:var(--ref-amber-500);--sys-error:var(--ref-red-500);
  --sys-overlay:rgba(0,0,0,.65);
  --sys-shadow-sm:0 1px 3px rgba(0,0,0,.25);
  --sys-shadow-md:0 4px 12px rgba(0,0,0,.3);
  --sys-shadow-lg:0 12px 32px rgba(0,0,0,.4);
  --sys-shadow-xl:0 24px 48px rgba(0,0,0,.5);
  --sys-icon-wt:1.75;--sys-font-wt:400;--sys-font-wt-md:500;
  --sys-font-wt-sb:600;--sys-font-wt-b:700;
  --sys-focus:0 0 0 3px var(--ref-brand-800);
  --sys-topbar-bg:var(--ref-gray-900);--sys-topbar-text:var(--ref-gray-100);
}

/* ---- SUNLIGHT (WCAG AAA — 21:1) ---- */
:root[data-theme="sunlight"] {
  --sys-bg:#FFFFFF;--sys-bg-alt:#FFFFFF;
  --sys-surface:#FFFFFF;--sys-surface-raised:#F1F5F9;
  /* v2.14.0: surface-raised lifted to slate-100 (a touch more contrast than
     light theme to suit Sunlight's high-vis baseline) so alternating rows
     and elevation gradients are still perceptible without compromising
     21:1 text contrast. */
  --sys-surface-blur:rgba(255,255,255,.95);
  --sys-surface-subtle:rgba(0,0,0,.04);
  --sys-text:#000000;--sys-text-sec:#000000;
  --sys-text-ter:#1a1a1a;--sys-text-inv:#FFFFFF;
  --sys-text-brand:#FFFFFF;
  --sys-border:#000000;--sys-border-strong:#000000;
  --sys-brand:#000000;--sys-brand-light:#E0E0E0;
  --sys-brand-hover:#222222;--sys-brand-dark:#000000;
  --sys-success:#000000;--sys-warning:#000000;--sys-error:#000000;
  --sys-overlay:rgba(0,0,0,.75);
  --sys-shadow-sm:none;--sys-shadow-md:none;--sys-shadow-lg:none;--sys-shadow-xl:none;
  --sys-icon-wt:3;--sys-font-wt:600;--sys-font-wt-md:700;
  --sys-font-wt-sb:800;--sys-font-wt-b:900;
  --sys-focus:0 0 0 4px #000;
  --sys-topbar-bg:#000000;--sys-topbar-text:#FFFFFF;
}

/* ---- SYSTEM (auto) ---- */
@media(prefers-color-scheme:dark){
:root[data-theme="system"]{
  --sys-bg:var(--ref-gray-950);--sys-bg-alt:var(--ref-gray-900);
  --sys-surface:var(--ref-gray-800);--sys-surface-raised:var(--ref-gray-700);
  --sys-surface-blur:rgba(30,41,59,.82);
  --sys-surface-subtle:rgba(255,255,255,.05);
  /* v2.21.0 contrast retune — mirror of explicit dark mode (see above):
     sec=gray-300 (9.85:1 AAA), ter=gray-400 (5.71:1 AA) on --sys-surface. */
  --sys-text:var(--ref-gray-100);--sys-text-sec:var(--ref-gray-300);
  --sys-text-ter:var(--ref-gray-400);--sys-text-inv:var(--ref-gray-900);
  --sys-text-brand:var(--ref-gray-0);
  --sys-border:var(--ref-gray-700);--sys-border-strong:var(--ref-gray-600);
  --sys-brand:var(--ref-brand-400);--sys-brand-light:var(--ref-brand-900);
  --sys-brand-hover:var(--ref-brand-300);--sys-brand-dark:var(--ref-brand-200);
  --sys-success:var(--ref-green-500);--sys-warning:var(--ref-amber-500);--sys-error:var(--ref-red-500);
  --sys-overlay:rgba(0,0,0,.65);
  --sys-shadow-sm:0 1px 3px rgba(0,0,0,.25);--sys-shadow-md:0 4px 12px rgba(0,0,0,.3);
  --sys-shadow-lg:0 12px 32px rgba(0,0,0,.4);--sys-shadow-xl:0 24px 48px rgba(0,0,0,.5);
  --sys-icon-wt:1.75;--sys-font-wt:400;--sys-font-wt-md:500;
  --sys-font-wt-sb:600;--sys-font-wt-b:700;
  --sys-focus:0 0 0 3px var(--ref-brand-800);
  --sys-topbar-bg:var(--ref-gray-900);--sys-topbar-text:var(--ref-gray-100);
}}

/* === RESPONSIVE TYPOGRAPHY v2.12.5 === */
/* TABLET (≥600px) — iPad mini portrait, small tablets */
@media (min-width: 600px) {
  :root {
    --ref-font-xs:   16px;
    --ref-font-sm:   18px;
    --ref-font-base: 21px;
    --ref-font-md:   24px;
    --ref-font-lg:   29px;
    --ref-font-xl:   34px;
    --ref-font-2xl:  40px;
    --ref-font-3xl:  48px;
    --ref-space-3: 14px;
    --ref-space-4: 18px;
    --ref-space-5: 24px;
    --ref-space-6: 28px;
  }
}
/* v2.16.0 T13: Desktop sidebar breakpoint — +4pt readability boost */
@media (min-width: 820px) {
  :root {
    --ref-font-xs:   19px;
    --ref-font-sm:   21px;
    --ref-font-base: 23px;
    --ref-font-md:   26px;
    --ref-font-lg:   29px;
    --ref-font-xl:   34px;
    --ref-font-2xl:  40px;
    --ref-font-3xl:  48px;
  }
}
/* iPad Pro 11"/13" portrait + landscape */
@media (min-width: 900px) {
  :root {
    --ref-font-xs:   16px;
    --ref-font-sm:   19px;
    --ref-font-base: 22px;
    --ref-font-md:   26px;
    --ref-font-lg:   31px;
    --ref-font-xl:   37px;
    --ref-font-2xl:  44px;
    --ref-font-3xl:  54px;
  }
}
@media (min-width: 1280px) {
  :root {
    --ref-font-xs:   16px;
    --ref-font-sm:   19px;
    --ref-font-base: 22px;
    --ref-font-md:   26px;
    --ref-font-lg:   32px;
    --ref-font-xl:   38px;
    --ref-font-2xl:  46px;
    --ref-font-3xl:  56px;
    --ref-space-3: 16px;
    --ref-space-4: 22px;
    --ref-space-5: 30px;
    --ref-space-6: 38px;
  }
}
@media (min-width: 1680px) {
  :root {
    --ref-font-xs:   17px;
    --ref-font-sm:   20px;
    --ref-font-base: 24px;
    --ref-font-md:   28px;
    --ref-font-lg:   34px;
    --ref-font-xl:   42px;
    --ref-font-2xl:  52px;
    --ref-font-3xl:  64px;
  }
}
@media (min-width: 2400px) {
  :root {
    --ref-font-xs:   20px;
    --ref-font-sm:   24px;
    --ref-font-base: 28px;
    --ref-font-md:   34px;
    --ref-font-lg:   42px;
    --ref-font-xl:   52px;
    --ref-font-2xl:  62px;
    --ref-font-3xl:  78px;
  }
}
/* === END RESPONSIVE TYPOGRAPHY === */
