=== SSC Career Hub === Contributors: ssc Tags: security jobs, cv review, careers, woocommerce Requires at least: 6.0 Requires PHP: 7.4 Stable tag: 1.12.11 License: GPLv2 or later Curated NY/NJ security job directory with full application pipeline tracking, CSV/Google Sheet sync, resume paste/upload review queue with shareable readiness reports, resume resubmit/version flow, public resume PDF sharing (external URL or hosted), FC-deck-style view analytics, attribution tracking, user credit ledger with 5-credit signup bonus that actually expires, mobile bottom tab bar, optional OpenRouter LLM enrichment for resume reviews and job-specific cover letters, admin courses catalog (5 seeded defaults) for scored-screen upsells, lightweight error logging, email-only soft capture, cookie consent lite, and admin pages for SSC Security Guard Training. V1.12.11 β€” Candidate-controlled share privacy + contact operations. Adds per-resume share controls for score visibility, resume new-tab access, share-link QR codes, and secure recruiter contact; adds safer Messages statuses (Interested/Replied/Archived/Spam), and an admin Contact Requests screen for moderation. Public recruiter pages stay clean and only show the sections the candidate enables. Keeps jobs, credits, WooCommerce, and AI rewrite flows unchanged. V1.12.10 β€” Clean hardening pass after share privacy + Messages. Fixes an important voice-polish cost-logging fatal caused by an old positional log_ai_usage() call, moves contact_requests schema into the main activator as well as the defensive init helper, includes contact_requests in uninstall cleanup when delete-data is enabled, and keeps share-link score privacy/contact defaults from v1.12.9. No redesign, no flow changes. V1.12.4 β€” Respectful credit metering (not Temu manipulation). Two pieces: (1) Credit balance pill in the account nav for logged-in users β€” small "πŸͺ™ N credits" tag, gold-tinted when balance > 0, gray when zero, links to /my-credits/ ledger. Informational, not promotional. No animations, no pulse, no scarcity copy. Hidden for logged-out visitors. (2) Inline cost preview on the use_credits checkbox in the submit form β€” replaces the hardcoded "Use 10 credits" (which didn't match service_cost) with dynamic "Spend N credits on the AI priority review Β· you have B" using the actual configured cost. When balance < cost: "you have B β€” need M more" honestly. Bilingual EN/ES. Honest transparency. v1.13 will add the positive-gamification layer (earn-by-completing-actions success moments, profile completion progress meters) β€” out of scope here to keep this release focused on visibility. V1.12.3 β€” Real document download after guided polish. After the 8-question Talk flow assembles a resume, the candidate now has THREE distinct outcomes: (1) βœ“ Use & get scored β€” fills the form, gets SSC readiness score + shareable link (existing in-platform path); (2) πŸ“₯ Download Word .docx β€” real .docx file via pure-PHP construction (no exec, no library bloat, just ZipArchive + 4 XML files); (3) πŸ“„ Save as PDF β€” opens print-styled HTML in new tab, auto-fires window.print(), user picks "Save as PDF" destination in browser dialog (zero server library). The textarea is now fully editable so candidates can tweak the polished version before downloading or using it. Honest helper copy explains the platform-vs-off split. NEW class SSC_CH_Doc_Builder with build_docx() (pure-PHP DOCX construction with headings/styles) + build_print_html() (self-contained print HTML). NEW endpoints ssc_ch_download_resume_docx and ssc_ch_print_resume_pdf, both nopriv (resume building works for logged-out candidates), both rate-limited (20/hr per IP) + nonce-checked. V1.12.2 β€” Mode picker replaces voice hero (a SWAP, not an add). Two equal cards instead of one dominant voice option: πŸ—£οΈ "Talk it out" (opens the guided 8-question modal directly) and πŸ“„ "Upload a file" (scrolls + triggers the file picker). Removed the awkward "Type or paste" card because raw-paste produces unformatted output that disappoints. Voice memo modal stays in DOM as hidden infrastructure (Talk β†’ guided still uses the same controller chain). Honest framing: header reads "Quick start" instead of misleading "Step 1 of 1" β€” the wizard underneath still collects name/email/role after the picker. V1.12.1 β€” AI rewrite with auto-spend after credit purchase + guided modal hotfix. THE BIG FEATURE: candidate clicks "✨ Rewrite with AI Β· 5 credits" on /cv-report/ β€” three branches: (1) already rewriting β†’ redirect to ?ai_pending=1 banner; (2) has credits β†’ deduct + schedule cron + redirect to ?ai_pending=1; (3) no credits β†’ store pending intent in user_meta + redirect to credit checkout with ?ssc_resume_to=/cv-report/?cv=N. After WC payment completes, maybe_credit_completed_order auto-fulfills the pending intent: deducts credits, schedules cron, clears intent. WC return URL filter bounces user back to /cv-report/?cv=N&credits_added=N, global toast confirms "Credits added", AI is already running. NEW columns ai_rewrite_text + ai_rewrite_at on cv_reviews (auto-added via DB_VERSION bump). NEW cron hook ssc_ch_run_ai_rewrite separate from admin-only review cron. NEW prompt strengthens weak dimensions while preserving every claim (no inventing credentials). NEW UI on /cv-report/: pending banner with spinner, result viewer with Copy/Use/Rewrite-again, error banner if cap exceeded with "no credits were charged" message. HOTFIX for v1.12.0 guided modal: graceful fallback when polish endpoint returns non-JSON or fails β€” uses the labeled Q&A transcript directly rather than showing "Network error". Pre-check word count client-side. Toast on rate-limit/cost-cap instead of error step. V1.12.0 β€” Guided question-asker for resume building (FundCollective-style). Sibling to the free-form voice memo: 8 fixed questions about a security candidate (name, location, phone, email, training, experience, target role, availability), each answered via voice OR type with a mode toggle. Progress dots at top, Back / Skip / Next navigation, Enter-to-advance keyboard support, Escape closes. On finish, formats answers as labeled lines ("Name: X\nLocation: Y...") and POSTs to the existing ssc_ch_polish_voice_memo AJAX endpoint β€” the existing OpenRouter prompt produces the structured resume regardless of input shape. Bilingual EN/ES questions. Voice answers use Web Speech API with auto-restart on browser timeout. Minimum 4 answered questions required to build. Result populates the cv_text textarea via "Use this resume" button. Opens via "Or answer 8 guided questions instead β†’" link at the bottom of the existing voice memo modal β€” both flows available, neither replaces the other. V1.11.10 β€” Vanity URL 404 fix. User reported sscresume.com/mekus returning 404 even though the handle was claimed. Most likely root cause: WordPress rewrite cache went stale and our /handle/ rewrite rule wasn't in the active rule set. Fix: (1) bumped SSC_CH_DB_VERSION β†’ 1.11.10 which forces maybe_upgrade() to re-run activate(), which re-registers the vanity rewrite + calls flush_rewrite_rules(false); (2) added diagnostic logging to maybe_serve() β€” every failure path (invalid handle format, shadowed by WP page, unclaimed handle) now writes to ssc_ch_abuse_events with the cause + handle, so the next 404 surfaces its reason in the admin abuse log instead of being invisible. V1.11.9 β€” /jobs/ page bundle. Four small fixes that all touch the same template: (1) Training/city dropdown highlighted row was light-bg + brand-blue text which read as white-on-light under WoodMart theme overrides β€” replaced with solid blue bg + white text, no var() fallback, universal contrast; (2) Filter strip now wrapped in
closed by default β€” "πŸ” Filters" pill at the top opens it on click; (3) Default view changed from Cards to List (data-view="list") so first-time visitors see the dense scannable list, returning visitors' localStorage choice still wins; (4) Defensive logging in bulk_job_action β€” the reported "0" blank page after Mark applied was likely wp_safe_redirect rejecting a cross-host referer; we now check the return value, log to ssc_ch_abuse_events as "bulk_redirect_failed" with the rejected target URL, and fall back to home_url('/jobs/') so the user lands somewhere usable instead of a blank screen. V1.11.8 β€” Native browser alert() popups replaced with toasts. The "www.sscsecurityguardtraining.com says: You need an external PDF URL set before enabling share" modal looked unprofessional; same for mic-denied, transcript-too-short, missing textarea, network error. New global helper window.SSCCH.toast(msg, isError) in frontend.js with the same visual language as the pipeline drag-and-drop toast (bottom-center, green=success/info, red=error, autodismiss). Five alert sites swapped: 2 in share-toggle AJAX error path, 3 in voice memo modal (mic denied, transcript too short, missing textarea). Each callsite falls back to native alert if SSCCH global isn't available (defensive). V1.11.7 β€” Settings URLs auto-cleaned to canonical /slug/ form. Root cause of the "?page_id=23892" URLs admin kept seeing: ensure_pages was writing whatever get_permalink() returned at activation time, and get_permalink returns "?page_id=N" if WordPress permalinks were on "Plain" at that moment (e.g. fresh install before admin set Post name). Once cached, every subsequent run kept overwriting with the same ugly URL. Fix: (1) ensure_pages now writes trailingslashit(home_url('/slug')) always; (2) one-time migration on DB_VERSION bump that sweeps existing settings and rewrites any URL containing "?page_id=" to canonical; (3) admin notice on every wp-admin page if permalink_structure is empty, with deep-link to Settings β†’ Permalinks. Applies to all 10 plugin URL settings (jobs_url, cv_optimizer_url, cv_report_url, cv_share_url, dashboard_url, my_cvs_url, share_links_url, pipeline_url, profile_url, my_credits_url). V1.11.6 β€” Manual resume rename. New display_name column on cv_reviews (auto-added on next admin pageview via DB_VERSION β†’ 1.11.6). Pencil ✏️ next to every title on /share-links/ β€” click swaps in an input, Enter or blur saves, Esc cancels. derive_resume_title now prioritises display_name β†’ target_role β†’ keyword detection β†’ "Security resume" generic. New AJAX endpoint ssc_ch_rename_cv with cv-scoped nonce + ownership check + 190-char cap + 20/hour per-user rate limit. Empty string clears back to auto-derived title. V1.11.5 β€” Zebra-stripe alternating row backgrounds on /share-links/ + /my-resumes/. After v1.11.3 (color stripe) and v1.11.4 (auto-title), candidates now get three independent visual signals per row: position-tied bg tone (even rows white, odd rows light gray-blue #f5f8fc), identity-tied stripe color (8-color palette via crc32(id)), and role-detected title. The same resume gets the SAME stripe color on /share-links/ AND /my-resumes/ β€” user memory carries between pages. /my-resumes/ rows now also have the left-edge color stripe (was only on /share-links/ before). V1.11.4 β€” Resume cards now have a meaningful title instead of "Untitled". New static helper `SSC_CH_Shortcodes::derive_resume_title($r)` falls back from explicit `target_role` β†’ keyword-detected role focus ("Concierge resume", "Fire guard resume", "Patrol resume", etc. β€” 17 keyword rules ordered by specificity) β†’ generic "Security resume". Applied at three call sites: /share-links/ panel header, /share-links/ vanity-claim dropdown options, /my-resumes/ row title. Both SELECT queries now pull `LEFT(cv_text, 600) AS cv_text_head` so the helper has source text for keyword scan. Also fixes the "Untitled CV" β†’ "Resume" terminology miss at /my-resumes/. V1.11.3 β€” /share-links/ resume cards now distinguishable at a glance. Three "Untitled resume" cards used to look identical (same title, same SSC-#000X format, same gray meta). Two minimal additions: (1) deterministic 6px left-edge color stripe β€” same resume ID always = same color from an 8-color palette, so user builds memory of "the green one is my driver resume, the gold one is concierge"; (2) human submission date ("Submitted 2 days ago") replaces SSC-# as the primary meta anchor β€” the SSC ref moves to a tooltip. Auto-name extraction + inline rename deferred to v1.11.4 (require schema change + UI surface). /my-resumes/ page may need the same treatment if user reports same identicality there. V1.11.2 β€” HOTFIX bundle. Voice polish silent-failure + /pipeline/ couldn't move cards. * VOICE POLISH FIX β€” Polish modal was returning the raw transcript verbatim in both "Cleaned version" and "Your original words" boxes. Root cause: the voice-polish endpoint hardcoded model 'anthropic/claude-3.5-haiku' while wp-config sets SSC_CH_OPENROUTER_MODEL='anthropic/claude-haiku-4.5'. OpenRouter returned 404 ("model not found"), polish fell back to raw transcript silently. Fixed by reading the model from SSC_CH_OpenRouter::model() helper which respects the constant. Added diagnostic logging β€” every polish failure now writes to ssc_ch_abuse_events with HTTP code + response preview so silent failures stop happening. Cost-estimate fallback now branches on model family (Haiku 4.5 at $1/$5 per 1M, Haiku 3.5 at $0.25/$1.25). * PIPELINE CARDS NOW MOVABLE β€” /pipeline/ shipped in v1.11.0 with a working pipeline_update admin-post handler but no UI to call it, AND the classification only looked at save/applied activities so cards never reached the later columns even if a stage was recorded. Both fixed: every card now shows a "← Back" / "Next Stage β†’" pair that POSTs to the existing handler with a stage-scoped nonce. Classification walks ALL pipeline activities and picks the latest one per job. The footer note "drag-and-drop coming in v1.12" replaced with "Use the buttons on each card to move it forward" so users stop waiting for drag-and-drop to use the feature. V1.11.0 β€” Dashboard slim + two new pages (/pipeline/ + /profile/). * SLIM DASHBOARD β€” Was 378 lines, now ~190. Goal: tell you what to do next when you log in. Greeting + activity feed + ONE recommended action + quick links. The submission-state, three-ways-to-push-score, recent-CVs, saved-jobs-table, application-pipeline-kanban that used to crowd the dashboard moved to their natural homes. * NEW /pipeline/ β€” Application tracker (Kanban: Saved β†’ Applied β†’ Heard back β†’ Interview β†’ Offer β†’ Hired). Pulls from ssc_ch_job_activity. Drag-and-drop comes in v1.12. * NEW /profile/ β€” Personal hub: vanity URL display, name, bio, contact details (phone, LinkedIn, website, Calendly), language. Save once, used on share pages and Apply Pack. * Account nav gets Pipeline + Profile tabs. * Dashboard next-action logic: explicitly does NOT chain "set up share" after "submit resume". Share link and resume audit are independent pillars per user feedback ("share url and upload cv are different"). * /cv-optimizer/ voice memo tile is now hidden in improve mode (?from=N). Voice memo is for first-timers without a written resume; updating an existing one needs editing, not re-recording. * Activator auto-creates /pipeline/ and /profile/ pages on first admin visit after upload (via DB version bump). V1.10.4 β€” Noise removal. Multiple flows compressed end-to-end. * AUDIT FLOW β€” Killed the intermediate /cv-optimizer/?submitted=1 page. Previously: submit β†’ mini-success page on the form β†’ click "Open full report" β†’ finally see report (2 page transitions, 1 wasted click). Now: submit β†’ straight to /cv-report/?just=1 with a green success banner at the top. 1 page transition. * FIXING FLOW β€” /cv-report/ reordered. The "Specific next steps for your resume" + a big gold "Improve & Resubmit" CTA now sit BEFORE the premium unlock card and QR code. Premium upsell moved to AFTER the helpful content where it belongs. The natural read is now Score β†’ Dimensions β†’ Next Steps β†’ Improve CTA β†’ Premium upsell β†’ QR/reviewer notes/share. * RESUBMIT FLOW β€” /cv-optimizer/?from=N (improve mode) jumps DIRECTLY to step 3 (resume text). Steps 1 and 2 are pre-filled and hidden by default β€” the user can flip back via the progress dots if they need to update their details, but the form lands them where the actual edit happens. From "see score β†’ resubmit" went from 5 page transitions to 2. * /MY-RESUMES/ COMPRESSED β€” The two big inline-styled section dividers ("πŸ“Š YOUR READINESS REVIEW" and "πŸ”— PUBLIC SHARE LINK") on every resume row were taking ~80px each. Replaced with a single compact "πŸ“Š Send your scorecard Β· Copy link" inline link. ~150px reclaimed per row. * VANITY URL FRIENDLY PLACEHOLDER β€” When a user claimed a handle but hasn't enabled public share on any resume yet, the URL was silently 404ing. Now it renders a styled "Setting things up" page with the owner's initial in a gold mark, their name, and a "Set up share β†’" CTA pointing at /share-links/. Strangers see "this URL is reserved but not live yet." * DB_VERSION bumped to 1.10.4 to force flush_rewrite_rules() on first admin visit after upload. Vanity URLs that weren't working because the rewrite cache had a stale rule will now resolve correctly. V1.10.3 β€” Removed redundant /cv-optimizer/ hero. The dark "Make your security resume interview-ready" hero with 4 pathway pills (Free first audit / Training clarity / Job-specific upgrade / Apply stronger) and the "What you get first" sidebar was duplicating the tutorial card above and the trust strip below. Six different banners stacked above the form was overwhelming for anxious low-tech-literacy candidates. The Employment-not-guaranteed legal disclaimer lives in the trust strip already. Page tab in the nav identifies the page. No info lost, ~400px vertical space recovered above the fold. V1.10.2 β€” HOTFIX. v1.10.1 vanity URL rewrite was registered at 'top' priority which intercepted EVERY root-level URL on the site (/shop/, /about/, /contact/, all root pages) and broke them. Fixed by switching priority to 'bottom' so WordPress's existing page/post/WooCommerce rules match FIRST and vanity URLs only catch unmatched single-segment URLs as a fallback. DB version bumped to 1.10.2 so maybe_upgrade() runs flush_rewrite_rules() automatically on first admin visit after upload β€” the broken 'top' rule is cached in wp_options['rewrite_rules'] and only gets wiped on flush. Also added defensive 404 in maybe_serve() β€” if a handle doesn't resolve to a user, we explicitly set_404() instead of leaving the query var hanging (which was causing WP to fall through to the blog index loop). V1.10.1 β€” Vanity URLs + Apply Pack + voice funnel + transcript backup + smarter voice flag + Spanish expansion. The "identity moment" release. * NEW β€” Vanity URLs (sscresume.com/maria-santos). New SSC_CH_Vanity class. Per-user handle in usermeta + per-user default-share cv. Rewrite rule matches /handle/ β†’ resolves user β†’ renders cv-share (uses standalone chrome-strip when admin enabled). 32-char max, lowercase only, no double-dashes, reserved-words blocklist covers wp-admin, jobs, share-links, etc. Inline AJAX availability check while typing β€” green "Available" / red "Taken" pill updates 350ms after keystrokes. Identity-frame copy: "This URL is yours forever. Print it on a card. Tell your family." * NEW β€” Apply Pack modal on /jobs/. Click Apply on any job β†’ instead of jumping straight to the employer site, modal shows the candidate everything they need: their resume URL (vanity or token), a copy-able contact line (Name Β· Phone Β· Email), and a generated 2-sentence intro tailored to the job ("Hi β€” I\'m Maria. I\'m applying for Concierge Security at LeFrak. You can see my resume at sscresume.com/maria. Happy to talk anytime."). Big "Open employer site & apply β†’" button. Dignity-frame copy: "You\'re ready. Take this with you." cmd/ctrl/shift-clicks still bypass to open in new tab (power-user respect). Esc-closes, click-outside-closes. * NEW β€” Voice funnel analytics. Each step of the voice-memo flow logs to abuse_events (our generic log table) with type prefix `funnel_voice_*`: modal_opened, unsupported, recording_started, recording_stopped, polish_succeeded, used_polished, used_raw, backup_recovered. Rate-limited 60/hr per IP. Admin can now see actual conversion through the funnel. * NEW β€” localStorage transcript backup. Saves transcript every 5 seconds during recording. On modal re-open, if a saved transcript < 30 minutes old exists, prompt: "We saved what you said before. Pick up where you left off?" β†’ polish runs on the saved transcript directly. Crash recovery for the iOS Safari "page reloaded during recording" case. * NEW β€” Voice flag auto-clear on heavy edit. If user picks the voice-polished version then rewrites more than 40% of the text by hand, the from_voice_memo flag clears to 0 β€” admin sees this as a typed submission, not a voice memo, because the original AI polish is no longer representative. * Spanish translations expanded: share-links page header + back-button, my-resumes page header + counts, 6 success/error banners. Plus all new vanity URL and Apply Pack copy is bilingual. Coverage now ~30% of candidate-facing strings. * Plugin activation now flushes rewrite rules so /handle/ URLs work immediately without a manual permalinks-save. V1.10.0 β€” Voice-memo-to-resume + Spanish UI + post-polish job-match glue moment. The "gold-level" release. * NEW β€” Voice-to-resume on /cv-optimizer/. Big gold "🎀 Tap to talk" tile at top of form. Candidate clicks β†’ modal opens β†’ picks English/Spanish β†’ records up to 60 seconds β†’ Web Speech API transcribes live β†’ OpenRouter polishes into a structured resume β†’ candidate picks "Use cleaned" or "Use my own words" β†’ resume drops into the wizard textarea and wizard auto-advances to step 3. For NY/NJ security guard candidates who can speak fluently but can't write β€” typically Spanish-dominant working-class immigrants. ~85-90% browser coverage (Chrome Android + Safari iOS 14.5+). Fallback shows "open in Chrome" message. * NEW β€” Bilingual UI (English / Spanish). New SSC_CH_I18n class + global ssc_ch_t() helper. Language toggle pill in account nav. Auto-detects from Accept-Language for first-time visitors. Persisted via cookie + usermeta. Spanish translations for: account nav labels, cv-optimizer hero copy, voice memo modal, error messages. Tutorials and rest of UI to be translated incrementally in v1.10.x. * NEW β€” Post-polish job-match GLUE MOMENT. Right after the voice memo polishes into a resume, JS scans the transcript for keywords (Brooklyn, fire guard, 8-hour, etc.) and shows a green celebration banner: "πŸŽ‰ Resume ready. Now: 3 jobs you can save right now." with tag-pills that deep-link to filtered /jobs/. Designed to convert peak motivation into immediate apply action. Candidate goes from "no resume" to "I have a resume + 3 saved jobs" in 5 minutes β€” the moment they tell their friends about us. * OpenRouter polish endpoint with full safety stack: claim-preservation prompt (never invents training/experience), max_tokens 800 cap, rate limit 5/hr per IP + 10/day per user, reuses existing is_cost_capped() daily budget cap, falls back to raw transcript on any API failure so user is never blocked. * Admin: 🎀 Voice badge on Resume Reviews list for voice-originated submissions. Raw transcript saved into admin_notes for AI hallucination audit. extraction_source = 'voice_memo' on the cv_reviews row. * Admin Gate Controls: two new kill switches β€” voice_memo_enabled and bilingual_ui_enabled. Both default ON. Admin can flip OFF if cost spikes or quality issues surface. * Plan + audience-fit critique saved to project. The original "Duolingo streaks + Instagram-style activity feed" plan was tech-bro psychology applied to the wrong market. Replaced with audience-fit features: voice memo, Spanish UI, WhatsApp/SMS (v1.10.x), vanity URLs (v1.11), apply-with-one-tap (v1.11). V1.9.10 β€” /share-links/ page restructure + tutorial state machine + internal linking. * /share-links/ existing cards now top of page (primary task = manage existing). "+ Create a new share link" is now a collapsed
below. Empty state opens the create form by default. Solves "noisy page" complaint where returning users had to scroll past a huge always-open create form to reach their existing share. * Tutorial two-state machine: STATE A (card visible, pill hidden) ↔ STATE B (card hidden, "? Show how this page works" pill visible). Click "Got it" goes Aβ†’B + saves localStorage. Click pill goes Bβ†’A + clears localStorage + scrolls back to card. Solves "if you hide tutorial should we bring it back? when needed" β€” yes, the pill is always findable. * Tutorial step internal linking: steps can now have an `href` field. If set, the step card becomes a clickable link with a β†’ arrow. On /share-links/: Step 1 "Fill in the form" β†’ links to #create-share which auto-opens the collapsed create form. Low-tech users follow the tutorial, click step 1, land in the form. * URL hash auto-opens
. Browsers don't do this natively. Now any link like /share-links/#create-share or /share-links/#cv-2 opens the matching details on load, recursively opens parent details, and smooth-scrolls to it. Wired into the same script as the chevron CSS. * Create form chrome dropped (no double-card-in-card since it now lives inside the collapsible details container). V1.9.9 β€” Silent-failure bug sweep + collapsible share-link cards. * CRITICAL bug fix: share_domain was silently unsaveable. apply_share_domain() called parse_url() on "sscresume.com" (no scheme) which returns ['path' => ...] not ['host' => ...], so the function bailed and returned the original URL unchanged. Now prepends https:// before parsing AND falls back to the bare hostname if parsing still fails. The setting WAS saved correctly all along β€” the application was broken. * Settings page sanitize had THREE silent-failure bugs in the same block. (1) share_domain via Sanitizer::http_url() silently stripped bare hostnames to empty (http_url requires scheme). Now uses local cleanup that accepts bare hostnames. (2) standalone_share_page checkbox: unchecked checkboxes don\'t post, so isset(...) ? : 1 always defaulted to 1 β€” admin could NOT turn it OFF, only ON. Fixed to !empty() pattern. (3) share_show_ssc_badge: same checkbox bug as #2. All three were silently failing on every Settings page save. * Each share-link card on /share-links/ is now collapsible via native
. Summary line shows title + ref + score + AJAX toggle pill + chevron. Click anywhere on the summary (except the toggle pill) to expand the card. Deep-linked card (#cv-N) and any share-OFF card auto-open. Solves user pain of "too many cards expanded at once is confusing". * Native ::-webkit-details-marker + ::marker suppressed so only the custom chevron renders. Chevron rotates 180Β° when open. Summary picks up a subtle background tint when expanded. * AJAX share toggle pill stops propagation so clicking it doesn\'t collapse the open card or expand the closed one. event.stopPropagation() inline handler ensures the pill behaves as an isolated control. V1.9.8 β€” Gate Controls page now actually renders the share-page settings (Custom share domain Β· Strip theme chrome Β· Show SSC badge Β· Logo removal cost). v1.9.4 added these settings to the sanitize_settings handler but never added the form fields, so admins couldn't change the share URL or hide the WoodMart header. Fixed. * Custom share domain field on Gate Controls β€” paste "sscresume.com" and every share URL the plugin renders uses that host. Domain is cleaned on save (strips https://, trailing slash, www). * Strip theme chrome checkbox β€” when ON, /cv-share/ renders as a standalone HTML doc with NO WoodMart header / footer / nav / cart icon. * Show "Verified by SSC" badge checkbox β€” controls whether the small SSC attribution shows on the recruiter-facing share page. * Logo removal credit cost field β€” set the credit price candidates pay to permanently strip the badge from one share page (default 10 credits). V1.9.7 β€” Plain-language tutorials on every page for non-tech-savvy candidates. Audience is NY/NJ security guards β€” low-tech, often mobile-only. Every candidate-facing page now shows a friendly "How this page works" tutorial card at the top with 3-4 numbered steps + a tip. * New SSC_CH_Shortcodes::render_page_tutorial($page_id, $title, $intro, $steps, $tip) helper. Renders a soft gradient card with avatar wave-icon, gold-accent kicker ("New here?"), numbered steps in an auto-fit grid, optional πŸ’‘ tip strip. Dismissable via localStorage per page_id β€” once a candidate clicks "Got it, hide this", that page's tutorial stays hidden on their device. * Wired into 7 surfaces: /jobs/, /my-dashboard/, /cv-optimizer/, /my-resumes/, /share-links/, /my-credits/, /cv-report/ (owner-only β€” recruiters viewing via share_token don't see the tutorial). Copy at ~5th-grade reading level β€” short sentences, no jargon, plain verbs. * Per-page step content tuned to the actual user task: jobs page explains filter + save + apply; submit-resume explains paste vs upload; share-links explains create + copy + send + track; cv-report explains score, bars, next steps, resubmit; credits explains free first / 10cr deep / 5cr cover letter / never-expire on purchased. V1.9.6 β€” Premium share page (FC pattern Β· level up the person sharing), AJAX one-click share toggle, admin re-run AI review. * Rebuilt /cv-share/ as a tinted-gradient deck-room layout: glassmorphic cards with backdrop blur, gold-accent avatar ring with green "Available" status dot, conic-gradient score dial with tone color, italic-pull headline with serif quote mark, FC-style contact card (LinkedIn / phone / website / email with hover slide animation), premium gold-gradient Calendly CTA with elevated shadow. Every visual decision tuned to make the candidate look like a top-tier professional, not a job applicant. * New AJAX one-click share toggle on /share-links/. Click the pill β†’ flips ON/OFF without a page reload. Mode-guard server-side (won't enable without a PDF source). Visual flip + label update in place. New endpoint ssc_ch_ajax_toggle_share, rate-limited 30/hour. * Admin can re-run AI review on already-scored resumes. New "↻ Re-run AI review" button next to the existing AI draft, plus a "+ Add AI review" button for non-priority scored resumes. force=1 query param clears ai_narrative + ai_narrative_at before the cron tick fires. * cv_share() now passes contact_email_display, contact_phone, contact_linkedin, contact_website, contact_calendly, short_bio, initials to the template β€” the FC sidebar renders all of them when present and gracefully hides the entire contact block when empty. V1.9.5 β€” Decoupled share link from resume review. New dedicated /share-links/ page with create-share-only form. FC-pattern contact details (phone, LinkedIn, website, Calendly, bio) on the share page. Resume review and share link are now truly independent β€” you can create a tracked share URL for your Google Doc resume without submitting a review. * New /share-links/ page with new shortcode [ssc_my_share_links] auto-created on upgrade. Dedicated "Share Links" tab in account nav. * New inline "Create share link" form on /share-links/ β€” paste a Google Doc / Drive / Dropbox URL + headline + bio + contact details + videos = instant tracked share URL. NO resume text required, NO review run, NO file upload. Decouples the two artifacts. * New handler ssc_ch_create_share_only β€” creates a cv_reviews row with status='share_only', share_token generated, public_share_enabled=1. * Schema bump 1.9.3 β†’ 1.9.5. ssc_ch_cv_reviews gains: contact_phone, contact_linkedin, contact_website, contact_calendly, contact_email_public, short_bio. dbDelta migrates existing installs idempotently on first authenticated pageload after upload. * /my-resumes/ now pure resume list β€” score, view report, improve, delete file only / remove entirely. Share controls moved to /share-links/. * /share-links/ now has a per-resume card for each resume showing toggle, video extras, view stats, reset URL, whitelabel. Each card has id="cv-N" so /my-resumes/ "Manage share link β†’" link deep-links to the right card. * New "Delete file only" vs "Remove resume entirely" distinction on /my-resumes/ β€” file deletion no longer kills the share link. Three explicit delete modes via delete_mode POST field. * Status-aware badge on unscored resumes: ⚑ Reading / πŸ‘€ Manual review / πŸ€– AI review running (replaces generic "Awaiting review"). * /cv-optimizer/ form rebuilt: explicit "OR" between Paste text and Upload file. One-time DB migration rewrites old "my CV" consent text to "my resume" for existing installs (only touches exact old default). * Sweep: every visible "CV" in admin menus, settings labels, KPI labels, email subjects, page titles β†’ "Resume". URLs, shortcodes, DB columns, class names stay on "cv" for backwards compat. V1.9.4 β€” Mega release: controls merge + chrome-strip share + custom share domain + logo removal credit + per-dimension narratives + comparative percentile + QR + named programs + premium unlock + Most Popular + Start Free Resume Audit + GET CERTIFIED button. * Schema bump 1.9.2 β†’ 1.9.3. `ssc_ch_cv_reviews` gains `logo_removed TINYINT(1) NOT NULL DEFAULT 0`. dbDelta migrates existing installs on first authenticated request after upload β€” no deactivate/reactivate required. * Controls merge β€” Cherry-picked the entire "SSC Career Hub Controls v1.0.0" companion plugin into the core plugin so installs only need ONE plugin going forward. New SSC_CH_Gate class handles: inline popup registration form (no redirect to wp-login.php), honeypot + rate-limit + nonce + existing-user-graceful pwd-reset, locked-down `ssc_job_seeker` role (no wp-admin, no admin bar, no comments), admin-editable gate copy (Gate Controls submenu page). Auto-deactivates the legacy companion plugin on upgrade β€” if v1.0.0 standalone controls plugin is active, plugin detects and deactivates on first admin request, surfacing an admin notice. * New: Chrome-strip standalone share page (FC-deck-tracker pattern). Setting `standalone_share_page` (default on) renders /cv-share/?cv=N&t=TOKEN as a clean standalone HTML document β€” no theme header, no theme footer, no menu, no WordPress chrome. Just the resume content with a slim SSC top bar: corner SSC brand mark + "Verified via SSC" badge + "More candidates β†’" link to the public Jobs page. Recruiters get a focused, professional view; SSC gets the credit. template_redirect hook intercepts at priority 5. * New: Custom share domain support. Setting `share_domain` rebuilds share URLs to swap the host β€” set "sscresume.com" and every share link rendered by cv_share_url() and cv_report_url() uses sscresume.com instead of sscsecurityguardtraining.com. User-purchased sscresume.com is ready to point at the same WordPress install; no further code changes needed when DNS is wired up. * New: Logo removal credit hook. Candidate can spend `logo_removal_credit_cost` credits (default 10 = $10) per resume to permanently strip the SSC corner badge from THAT resume's share page. UI: "Remove SSC logo" button on /my-resumes/ share-toggle panel, gated by current credit balance. Atomic spend: charges credits + flips `logo_removed=1` in a single transaction with rollback on failure. Per-resume, irreversible from the candidate side. Admin can override with "Reset logo flag" tool (v1.10 planned). * New: Per-dimension narratives on /cv-report/. Replaces v1.9.3's bar-only score display. Each of the 6 dimensions (licence_training, security_fit, experience, availability_location, clarity, role_match) now renders a 1-2 sentence narrative tuned to BOTH the candidate's actual score in that dimension AND their state (NY DCJS vs NJ SORA β€” distinct copy). Three tier breakpoints at 65% / 35% / below β€” high tier reinforces strengths, mid tier prescribes specific fixes, low tier flags as a recruiter-stop-reading risk. New SSC_CH_CV_Analyzer::dimension_narrative($dimension, $score, $max, $state) helper. * New: Comparative benchmark percentile on /cv-report/ hero. Pill badge shows "You scored higher than X% of NY candidates this month" with cohort math: same state, scored cohort, last 30 days. Falls back to lifetime when state-cohort < 5 to avoid misleading single-sample percentiles. Returns null cleanly when even lifetime < 5 (badge hidden). New SSC_CH_DB::benchmark_percentile() helper returns {percentile, cohort_size, window_days}. * New: Named programs as next-step CTAs on /cv-report/. Mid-page premium unlock section presents ONE named program matched to the candidate's score tier (FC-pattern depth): - 0-49: "Build the Base" β€” 8-hour DCJS + base resume + first applications. Pitched as urgent foundation work for thin resumes. - 50-64: "Resume Rewrite Sprint" β€” 5-day rewrite + cover letter + 3 priority applications. Pitched as the "stop being generic" move. - 65-79: "Cover Letter Builder" β€” 5 tailored cover letters + recruiter-message templates + interview prep. Pitched as the "convert applications to interviews" move. - 80+: "Application Pack Week" β€” Top 20 NY/NJ openings + 5 cover letters + LinkedIn polish + interview coaching. Pitched as the "close out fast" move. Each program has eyebrow, headline, pitch, price label, primary CTA + softer "Just buy credits" secondary CTA. Dark gradient card with gold accent. Visible to candidate-owner only. * New: QR code generation on /cv-report/ (owner-only). 150x150 PNG via quickchart.io/qr (no library, no dependency, no API key). Copy: "Print on a business card, paste in a Slack/Teams message, or have recruiters scan it at a job fair." Click "Open larger PNG β†—" for a 600x600 version. QR encodes the share URL (not the report URL) so scans go straight to the recruiter-facing share page. * New: Most Popular badge on 10-credit pack. SSC_CH_Credits::packs() now carries a `popular` flag. SSC_CH_WooCommerce::render_topup_cards() injects `is-popular` class on the 10-credit card, rendering a blue "Most popular" pill above it. Outcome-led pack titles applied across both Credits and WooCommerce render paths: 5 β†’ "Quick Polish", 10 β†’ "Ultimate Resume Review", 20 β†’ "Application Pack", 50 β†’ "Priority Career Bundle". * New: "Start Free Resume Audit" CTA + trust strip on cv-form.php. Wizard step-2 "Continue β†’" renamed "Start Free Resume Audit β†’". Step-3 submit button "Get my free resume audit" β†’ "Start Free Resume Audit" with microcopy below: "Audit takes under 60 seconds Β· score + per-dimension narrative Β· share link generated automatically". New trust strip above the form: "Free first audit Β· NY DCJS / NJ SORA focused Β· Training-aware review Β· No spam Β· 24-hour SLA Β· Employment not guaranteed". * New: GET CERTIFIED button styling. Dedicated `.ssc-ch-cert-button` class for the training-route CTAs across the plugin. Spec: dark navy #071426 background, white uppercase text, 3px border-radius (not pill), 50px min-height, 30px horizontal padding, 900-weight, .055em letter-spacing, no gradient. Hover lift + shadow only. Applied via `.ssc-ch-training-route` selector too for backwards compatibility. * Admin: New "Gate Controls" submenu page. Edit the popup gate title, body, button label, success message, error message + toggle the cookie banner + adjust the guest jobs limit + set the custom share domain + toggle the SSC badge on share pages + set the logo-removal credit cost. All stored in `ssc_ch_gate_settings` option key β€” same as the legacy companion plugin used, so existing settings carry over on upgrade. * Mandatory protocol check β€” every step of v1.9.4 verified against CLAUDE.md's "Mandatory Protocol: Reflect After Every Step": syntax verified, adversarial review questions asked at each stage, sibling pages audited for the same pattern, UX gut check on every change (does this make the experience more seamless or more SaaS-like?). V1.9.3 β€” Big release: share page videos + owner Stats panel + Jobs date filter + rename sweep finalised. * Schema bump 1.9.1 β†’ 1.9.2. `ssc_ch_cv_reviews` gains `intro_video_url`, `demo_video_url`, `headline` (all NULL by default β€” empty share pages render unchanged). `ssc_ch_report_views` gains `country`, `city`, `device`, `browser`, `is_return` populated on every view insert. dbDelta handles existing installs idempotently β€” first authenticated page load after upload triggers the migration. * New: candidates can attach two video URLs + a headline to each shared resume independently. Share toggle panel on /my-resumes/ gets a "Optional extras for the shared page" details block with three inputs: Headline / tagline (190-char limit), Intro video URL, "Second video URL" (optional). YouTube, Vimeo, and Loom auto-detected and rewritten to embed URLs. Headline renders below the candidate name on the share page; videos render in a 16:9 grid below the resume iframe with tag chips ("Intro" / "Why this role"). Cleanly hidden if not filled. * New SSC_CH_DB::detect_video_embed($url) helper handles YouTube (youtu.be/, watch?v=, embed/, shorts/), Vimeo (vimeo.com/ID, /video/ID), and Loom (share/, embed/). Returns {type, embed_url}. * New: per-resume owner-facing Stats panel on /my-resumes/. Appears as a collapsible "πŸ“Š View stats" details element below the share toggle for any resume with at least 1 non-owner view. Summary line shows total views + last view relative time + "πŸ”₯ return visits" badge if any. Opens to: 6-card KPI strip (total, unique, avg time, max scroll, top country, top device), then a "Recent views" table (last 10) showing each view's relative time, location, device + browser, time on page, scroll %. Return-visit rows tinted red with a "return" tag β€” instant hot-lead surfacing. * New SSC_CH_DB::recent_views_for_cv() + view_stats_for_cv() β€” single query each, no N+1 on the /my-resumes/ page. * New tracking columns populated on every view: country (via Cloudflare CF-IPCountry header β€” free when behind CF, otherwise NULL until v1.11's ipinfo.io integration adds city + country fallback), device (mobile/tablet/desktop via UA regex), browser (chrome/firefox/safari/edge/opera/other via UA regex), is_return (true when same viewer_hash already has a row for this resume). * New SSC_CH_DB::detect_device() + detect_browser() + detect_country() helpers. * Jobs page: new "Posted" filter (any time / 24 hours / 7 days / 14 days / 30 days) wired into get_jobs() β€” filters by COALESCE(verified_at, created_at) so jobs missing a verified_at still date-filter correctly. Each job card now shows "Posted N ago" inline above the verified badge (e.g. "Posted 3 days ago"). Helps candidates spot fresh listings without scanning all cards. * Resume rename sweep finalised β€” caught 12 remaining "CV" strings across class-ssc-ch-shortcodes.php: "Strong Security CV" verdict β†’ "Strong Security Resume", credit balance copy, no-credit-activity empty state, login-required gate, share conflict messages, "Make this CV publicly viewable" toggle label, "upload one when submitting your next CV" hint, Google Drive tutorial copy, "Public CV sharing is disabled site-wide" notice, email-intro mailto body, "CV file missing" share page fallback, recruiter intro mailto body, "CV not found" + "Public CV sharing is disabled" wp_die messages. UI text is now 100% Resume. V1.9.2 β€” Smart embed detection on share pages (FC-deck-tracker pattern). No more Google "You need access" walls. * CRITICAL β€” Candidate-pasted share URLs are now auto-transformed to the correct preview variant. Pasting a Google Drive `/file/d/ID/view` rewrites to `/file/d/ID/preview` automatically. Pasting a Google Docs `/document/d/ID/edit` rewrites to `/document/d/ID/preview`. Pasting a Google Slides `/presentation/d/ID/edit` rewrites to `/presentation/d/ID/embed?start=false&loop=false`. Pasting a Dropbox `?dl=0` URL rewrites to `?raw=1`. Most candidates copy-paste the wrong variant; this catches them silently. * New `SSC_CH_DB::detect_embed($url)` β€” ports FC's 4-path detection: PDF (any HTTPS host with `.pdf`) β†’ iframe with sandbox; Google Drive/Docs/Slides β†’ iframe with the corrected `/preview` URL; Dropbox β†’ iframe with `raw=1`; OneDrive β†’ iframe; anything else β†’ branded "Open in new tab" button fallback. Returns `{type, url, label}` consumed by cv-share.php. * cv-share.php template rebuilt to render per detected embed type. Recognized embeddable URLs get the iframe (with `allow-popups` added to sandbox so Google's auth popup can open if needed). Unrecognized URLs get a polished branded fallback card β€” no more broken iframe showing Google's "You need access" wall inside our chrome. The button-fallback explicitly says: "Hosted on an external service. Click below to view in a new tab" with a single primary CTA. * Embed-source label visible in the chrome β€” "Open Google Drive β†—", "Open Google Docs β†—", "Open Dropbox β†—" etc. instead of generic "Open PDF β†—", so recruiters know exactly where they're going. * Helpful host note below the iframe: "Hosted on Google Drive. If the preview shows 'You need access', the candidate hasn't made the file public yet β€” use the Open Google Drive β†— button above instead." Tells the recruiter what to do without making the candidate look bad. V1.9.1 β€” Completion patch: closes v1.9.0's two intentional gaps + sweeps remaining rename strings + extends share buttons to every scored surface. * CRITICAL β€” Credit expiry now actually works. `expires_at DATETIME NULL` added to ssc_ch_credit_ledger via dbDelta (idempotent on existing v1.8.x β†’ v1.9.x installs). `SSC_CH_DB::user_credit_balance()` filters out expired positive credits (negative spend/refund entries always count). `grant_signup_credits()` now sets `expires_at = NOW + 14 days` so the welcome message's "expire in 14 days" claim is true and enforced. The My Credits ledger surfaces expiry inline on each row: "expires May 11 (8 days left)" or "expired May 4" if past. No cron needed β€” filter-on-read enforces it. * CRITICAL β€” 5 default courses seeded into ssc_ch_courses on activate/upgrade: NY 8-hour DCJS Pre-Assignment ($99, +25 lift, not_trained), NY 16-hour OJT ($149, +18, completed_8_hour), NY 8-hour Annual ($79, +10, renewing), NJ SORA Phase I ($199, +25, not_trained), NJ SORA Phase II ($259, +18, nj_sora). Each carries a placeholder course_url pointing at SSC's existing WooCommerce product slugs (admin updates URLs once if structure differs). Idempotent β€” INSERT IGNORE via UNIQUE KEY on slug. Means the scored-screen 3-CTA card shows 3 CTAs out-of-the-box on every install instead of 2. * New admin Courses page at /wp-admin/admin.php?page=ssc-career-hub-courses β€” list + add + edit + delete UI for the courses table. Inline edit form with all fields (slug, name, description, URL, price, lift, target state, target training status, sort order, active toggle). Built with standard WP admin patterns + capability gate (ssc_ch_manage_settings). Replaces the v1.9.0 placeholder where admin had to INSERT rows directly via phpMyAdmin. * One-click share buttons now wired to ALL scored surfaces β€” not just cv-form.php submission state. Dashboard's post-submission state (`/my-dashboard/?submitted=1&cv=N&t=TOKEN`) calls `SSC_CH_Shortcodes::render_scored_actions()` so logged-in candidates redirected there after submit get the same 3-CTA + share row as anonymous users on cv-form.php. /my-cvs/ scored resume cards now show a Copy/LinkedIn/WhatsApp/Email share button row inline beneath the actions row. Share artifact is identical across all 3 surfaces (uses /cv-report/?cv=N&t=TOKEN URL β€” branded slugs land v1.10). * Final rename sweep β€” caught 8 remaining "CV" user-facing strings across dashboard.php and cv-report.php that survived the v1.9.0 pass: "Your CV scored X/100" β†’ "Your resume scored", "CV reviews" KPI label β†’ "resume reviews", "CV review history" aria-label, "CV score trend" heading, "Where your CV scored" dimension heading, "Get ultimate CV review" CTA, "CV rewrite + role-fit notes" caption, "SSC-CV-#NNNN" reference codes β†’ "SSC-#NNNN". Templates now 100% renamed across visible UI. V1.9.0 β€” Activation foundation: brand + conversion + compliance + WCAG baseline. Big release. * CRITICAL β€” Plugin moved from "feature tree" to "activation funnel". 5 named candidate personas (Marcus career-switcher / Aisha active hunter / Ricky upgrade-blocked / Daniel veteran / Jasmine positioning gap) can now walk through the product without hitting a dead end. * Resume rename across all user-facing text. "CV" β†’ "Resume" in templates, shortcodes, emails, OpenRouter AI prompts (system + user). Internal names unchanged: URLs (/cv-optimizer/, /cv-share/, /cv-report/), shortcodes ([ssc_cv_optimizer], [ssc_my_cvs]), DB columns (cv_text, cv_id), PHP class names (SSC_CH_CV_Analyzer), CSS classes (.ssc-ch-cv-*). Migration risk = zero; existing share links keep working. * Reference codes simplified: SSC-CV-#NNNN β†’ SSC-#NNNN (less ambiguous, no internal-terminology bleed). * New: Activation banner above Jobs hero for non-members. "🎯 Get your free resume readiness score Β· 60 seconds Β· See where you stand for NY DCJS / NJ SORA jobs" with primary CTA. Dismissible after first submission. * New: Member teaser. Non-members see 7 jobs (admin-configurable 3-15 via guest_jobs_limit setting) followed by a "N more jobs Β· Sign up free to see them all" CTA. Conversion-driven without being aggressive. * New: Login wall on Apply. Non-members clicking Apply on any visible job get a polished modal (bottom-sheet on mobile) explaining the signup value: "Your resume stays saved for the next 30 applications Β· one-click apply Β· pipeline tracking Β· 5-credit welcome pack." Two CTAs: Sign up free + Already a member? Log in. * New: Scored screen 3-CTA upsell card. After resume submission, candidate sees their score + score dimensions + THREE action cards: (a) Most relevant SSC course (matched by training_status + state via ssc_ch_courses table β€” hides if no match), (b) Pro plan offer with 10-credit pack as fallback, (c) Free 5-minute improvement via resubmit. Each card shows projected score lift. New SSC_CH_Shortcodes::render_scored_actions() shared helper. * New: One-click share buttons on the scored screen. Copy URL / LinkedIn share dialog / WhatsApp click-to-share / Email mailto: with prefilled subject + body referencing the score. Buttons live in a reusable SSC_CH_Shortcodes::render_share_buttons() helper, ready to deploy on dashboard + my-resumes cards in v1.10. * New: Email-only soft capture for non-members. After scoring (non-logged-in path), a dashed-border card invites "Want your score emailed to you?" with explicit GDPR consent line. Captures emails into new ssc_ch_email_captures table for the nurture digest. Rate-limited 3/day per IP. Idempotent via (email, source) unique key. * New: 5-credit universal signup grant. Fires on WordPress user_register hook β€” every new account gets 5 credits with 14-day shelf life regardless of how they signed up (CV submission, browse-only, future auth paths). Covers exactly ONE cover letter trial (the headline AI feature). Idempotent β€” duplicate user_register events don't double-grant. * New: ssc_ch_courses table for the scored-screen upsell. Schema: id, slug, course_name, course_description, course_url, price_usd, score_lift_estimate, target_training_status, target_state, sort_order, is_active. Admin will populate with SSC's 8-hour DCJS, 16-hour OJT, NJ SORA Phase I/II, annual renewal courses. * New: ssc_ch_error_log table + SSC_CH_Logger class. Replaces silent error_log() calls with structured rows (severity, route, error_message, context_json, user_id). Will surface in Abuse Events admin alongside existing entries via severity filter (v1.10 wire-up). * New: ssc_ch_email_captures table with explicit GDPR consent text hash for audit trail. * New: Empty / loading / error state design system. Three reusable CSS classes (.ssc-ch-state-empty / .ssc-ch-state-loading / .ssc-ch-state-error) with consistent layout β€” icon + headline + body + 1 CTA. Applied to My Resumes empty state, Jobs no-matches state. Tone-of-voice guide at /docs/copy-guide.md mandates every empty state propose a next action. * New: Cookie consent lite banner. Admin opts in via Settings β†’ Privacy. Renders only if no dedicated cookie plugin (CookieYes / Cookie Notice / Moove GDPR) is active β€” defers to them when present. Reject clears the ssc_ch_attr UTM cookie immediately. Choice persisted in localStorage. * New: Soft cap warning + hard cap helpers. SSC_CH_DB::cap_status($used, $limit) returns {used, limit, percent, status} where status ∈ {ok|warn|hit}. Foundation for v1.10's multi-link cap logic + v1.12's plan-based limits. * New: WCAG AA baseline. Skip-to-content link at top of Jobs page (visible only on Tab focus). 3px outline focus indicators on every interactive element. prefers-reduced-motion media query honored β€” analyzing-overlay pulse, scored-action hover animations skipped for users with reduced-motion enabled. All touch targets audited to β‰₯ 44Γ—44px (Apple HIG standard). * New: Mobile polish pass. Login-wall modal slides up as bottom-sheet below 720px. Share buttons reflow to 2-column grid on small screens. Scored-action 3-CTA grid collapses to single column. Activation-banner CTA goes full-width below 720px. * New: docs/copy-guide.md β€” one-page tone-of-voice guide. Word choices, forbidden phrases ("seamlessly", "leverage", "world-class" out), CTA verb rules, NY DCJS / NJ SORA disambiguation, empty/error state conventions. Referenced by every future copy decision. * New: SSC_CH_Shortcodes::render_scored_actions() + SSC_CH_Shortcodes::render_share_buttons() + SSC_CH_Shortcodes::render_email_capture_form() β€” three reusable render helpers ready for v1.10 to call from dashboard + my-resumes cards once those surfaces are touched. * DB schema bumped to 1.9.0. maybe_upgrade() handles existing v1.8.x installs idempotently β€” three new tables added via dbDelta, zero changes to existing tables. V1.8.12 β€” Frontend JS hardening: every init step is independently fault-tolerant: * User reported that several clickable elements stopped working on the Jobs page (Save, Applied, Star, Details toggle, Cards/List view switch, Clear button). Root cause: JavaScript's `Array.prototype.forEach` propagates exceptions from any iteration callback to its caller. If our combobox init (added in v1.8.9) threw on a single iteration β€” for any reason: cached old JS, content-filtered HTML, missing DOM nodes, mismatched payload β€” the exception bubbled up through `initMultiSelectFilters()` β†’ `initHub()` β†’ the rest of the Jobs-page wiring never ran. Filter form bound no submit/change listeners, result-row click handlers never bound, view-toggle dead, Clear button dead. One broken init was silently breaking the entire page. * New `safely(label, fn)` helper wraps each init step. If it throws, the error is logged with `[ssc-ch] FAILED: