=== SSC Career Hub ===
Contributors: ssc
Tags: security jobs, cv review, careers, woocommerce
Requires at least: 6.0
Requires PHP: 7.4
Stable tag: 1.8.0
License: GPLv2 or later

Curated NY/NJ security job directory with full application pipeline tracking, CSV/Google Sheet sync, CV paste/upload review queue with shareable readiness reports, CV resubmit/version flow, public CV PDF sharing (external URL or hosted), FC-deck-style view analytics, attribution tracking, user credit ledger, mobile bottom tab bar, optional OpenRouter LLM enrichment for CV reviews and job-specific cover letters, and admin pages for SSC Security Guard Training.

V1.8.0 — public CV PDF share:
* New [ssc_cv_share] shortcode — public, token-shareable page where recruiters view a candidate's CV PDF. Sticky top bar carries candidate name, target role, NY-DCJS / NJ-SORA track, location, score badge (if scored), one-click Contact button (mailto with prefilled subject), copy-link, and breakdown-link. PDF rendered in a generous viewport iframe below.
* Per-CV opt-in toggle on /my-cvs/ — candidate chooses which CV(s) to make public. Off by default. View counter visible to candidate.
* Admin share_mode global setting (Settings page): url_only (recommended — candidate hosts on Drive/Dropbox/OneDrive, less SSC liability) / upload_only (SSC hosts) / both / off.
* Practical hosting tutorial collapsible in the candidate's share-toggle UI: exact Drive /view → /preview swap, Dropbox ?dl=0 → ?raw=1 swap, and OneDrive embed-URL extraction.
* Security: External URL sanitiser rejects non-HTTPS, javascript:/data: schemes, URLs with embedded credentials (parse_url rebuild strips them), and URLs > 500 chars. Local PDF serve endpoint validates token via hash_equals, basename()s the stored filename (no path traversal), magic-byte check on every serve (rejects anything not starting with %PDF-), responds with Content-Type: application/pdf + X-Content-Type-Options: nosniff + inline Content-Disposition. External-URL iframe gets sandbox="allow-scripts allow-same-origin" (no popups, no top-nav escape). Toggle handler rate-limited at 15/hour per user.
* Settings-conflict warning surfaced to candidate when admin switches share_mode after they've enabled sharing in the opposite mode — tells them exactly what to fix (e.g. "Site is set to URL-only sharing — paste an external URL").
* Engagement tracking reused from [ssc_cv_report] beacon (visibilitychange + pagehide → sendBeacon with cv_id + token + seconds + scroll%). New view_type='cv_pdf' column on ssc_ch_report_views so CV-share views are separable from score-report views.
* New DB columns on ssc_ch_cv_reviews: public_share_enabled (default 0), external_pdf_url (max 500 chars), share_pdf_views. New column on ssc_ch_report_views: view_type (default 'report'). dbDelta auto-applies via maybe_upgrade on init priority 1.
* Auto-create /cv-share/ page on activation/upgrade (joins the existing 6 auto-pages).

V1.7.0 — closing deferred gaps:
* Per-CV view-count column on admin CV Reviews. Total views + unique viewers + last-view timestamp per CV, rendered in a new column. Single batch query (COUNT, COUNT DISTINCT, MAX created_at GROUP BY cv_id) avoids N+1 on the admin row render.
* CV resubmit / version flow. "Improve this CV →" button on Dashboard Recent CVs cards and on [ssc_my_cvs] cards. Clicking it goes to /cv-optimizer/?from=N. Form prefills cv_text, state, training, target_role, city from the user's previous CV (verified via wp_user_id match — no cross-user leakage). Hidden parent_cv_id field passed through upload handler → stored in new cv_reviews.parent_cv_id column. Yellow "Improving CV-#NNNN · previous score X/100" banner at top of form.
* Mobile bottom tab bar. Below 720px the sticky-top account nav switches to a fixed-bottom 5-tab bar with icon glyphs (⌂ Home, ▤ Jobs, + Submit, ☰ CVs, ★ Credits) and short labels. Active tab gets a pill-style background highlight. Spotify / Wise / Robinhood pattern. Body padding scoped via body:has(.ssc-ch-account-nav) so non-plugin pages aren't affected.
* Cover-letter LLM generator (admin-only V1). New SSC_CH_OpenRouter::generate_cover_letter() method follows the same prompt-injection-safe pattern as the CV narrative (fixed system role, ===CV BEGINS=== / ===JOB DESCRIPTION BEGINS=== markers, 200-260 word cap, no AI self-disclosure). Admin opens a <details> panel on the CV Reviews row, pastes the job description (min 80 chars), clicks Generate. Result stored in new cv_reviews.cover_letter_text / cover_letter_at columns. Admin reviews + manually shares with candidate. Cost logged to ai_usage with :cover_letter model suffix for separate tracking.
* New DB columns: cover_letter_text MEDIUMTEXT, cover_letter_at DATETIME, parent_cv_id BIGINT UNSIGNED. dbDelta auto-applies via maybe_upgrade on init priority 1 (no deactivate/reactivate needed).

V1.6.1 — final sanity-pass hotfix on top of 1.6.0:
* maybe_upgrade now runs on the `init` hook (priority 1) instead of `admin_init`. The previous wiring only triggered schema auto-upgrade when an admin visited wp-admin — so a public user submitting a CV after a plugin upload but before any admin pageload would hit the stale-schema cv=0 bug again. Now schema check fires on every request (one cached get_option + version_compare; cost is negligible). Closes a real race-window hole in 1.4.9's self-healing install.
* SSC_CH_DB::user_application_pipeline() SQL fixed for MySQL 5.7+ strict mode. The previous query had MAX(a.created_at) as an aggregate alongside non-aggregated j.title, j.company, etc., with GROUP BY j.id, a.activity_type — which ONLY_FULL_GROUP_BY (default in modern MySQL/MariaDB) rejects, returning an empty pipeline silently. The inner subquery already constrains the result to exactly one activity row per job, so the outer MAX() and GROUP BY were both redundant. Now uses a.created_at directly with no outer GROUP BY. Pipeline kanban will populate correctly on production MySQL.

V1.6.0 — page-by-page parity pass:
* FC-deck-tracker parity on the CV report. Owner-only "Who's viewed your report" panel: total views, unique viewers, last viewed (relative time), average time-on-page, deepest scroll, plus a collapsible "Recent views" list (date, viewer hash short, referrer host, seconds, scroll percent). A JavaScript beacon on the report page tracks time-on-page + max scroll depth and fires once on visibilitychange='hidden' / pagehide via navigator.sendBeacon (fallback fetch keepalive). Engagement is appended to the existing ssc_ch_report_views row matched by cv_id + viewer_hash. Token verified server-side with hash_equals before any write.
* New ajax endpoint: ssc_ch_track_report_engagement (logged-in + anonymous variants). Token check, length validation, hash_equals, sanitised payload (max 7200s, max 100% scroll).
* Print-friendly CV report. New @media print stylesheet hides nav, share box, engagement panel, WP admin bar; preserves score dial + dimension bars via print-color-adjust; appends URL after each link except in-app navigation. "Print / Save as PDF" button on the owner-facing share row uses window.print(). Recipients (employers, mentors) can save a clean PDF copy of the report without any extra plugin.
* Application pipeline on the Dashboard. New Trello/Linear-style kanban board with 6 columns: Saved, Applied, Heard back, Interview, Offer, Hired. Each saved job appears as a card in its current stage; user changes stage from a per-card dropdown which auto-submits via admin_post (nonce + rate-limit protected, 30 transitions/hour per user). New activity_type values added to record_activity allowlist: heard_back, interview, offer, rejected, hired, withdrew. New SSC_CH_DB::user_application_pipeline() returns the latest stage per (user, job) pair joined to the jobs table.
* User-facing credit ledger on /my-credits/. Shows last 25 credit transactions in a clean table (date, activity type, +/- credits with tone colour, note). Friendly labels for purchase / spend / admin_adjustment / refund. Empty state guides first-time users to top up.
* Copy share-link button on /my-cvs/ cards (parity with the Dashboard Recent CVs section).
* "Email intro" button on every job card. Generates a polished pre-written intro (user's first name, target role, company, location, NY/NJ track, available start date) and opens the user's mail client via mailto:. Detects whether apply_url is a mailto: (uses that recipient) or a regular URL (recipient blank so user can paste into the employer's form).
* New SSC_CH_DB helpers: report_engagement(), update_report_engagement(), user_credit_ledger(), user_application_pipeline().

V1.5.1 friction reducers + magnetic UX:
* CV form now prefills name, email, phone, and city from the logged-in WP user profile (first/last name → name, user_email → email, billing_phone + billing_city meta if WooCommerce). A small green note tells the user "Prefilled from your account — edit if you want different details". Removes 4 fields of friction for returning members.
* Personalized greeting on Dashboard. "Good morning, Edward" / "Good afternoon, Edward" / "Good evening, Edward" based on time of day. H2 also flips from generic "My Security Career Dashboard" to "Edward's career dashboard" when first name is known.
* Smart "Next move" card on Dashboard. Single CTA that adapts to where the user is in their journey:
  - No CV yet → "Submit your first CV" → /cv-optimizer/
  - Has a scored CV but score < 65 → "Push your score to 80+" → opens their latest report
  - Has CV but no saved jobs → "Save 3 realistic jobs" → /jobs/
  - Has saves but no applies → "Apply to 1 today" → /jobs/
  - Active user → "Refresh your CV monthly" → /cv-optimizer/
  Bold gradient banner with a radial glow accent, single white pill CTA. Never a dead end — always one specific thing to do next.
* Smooth hover micro-polish applied to every card across pages (recent CVs, side cards, submission tiles, top-up cards, quick actions). Unified .15s ease transition so the whole UI feels cohesive when the user moves through it.

V1.5.0 unified account hub + flow consolidation:
* CV submission now redirects to the Dashboard (not back to the CV form). After submit, the user lands on /my-dashboard/?submitted=1&cv=N&t=TOKEN[&score=N] with the same status panel (icon, reference number, timeline, primary actions). This answers "what happens after submit" — the user is on their account hub with the score front-and-centre, not stranded on the form page.
* Dashboard catches the submission params at the top and renders the status panel above the rest of the dashboard content. Stats, score trend, recent CVs, and saved jobs remain below — so the user sees their result AND their broader account state in one place.
* Account nav is now sticky at the top of the viewport across every member page. Sits below the WP admin bar (32px or 46px on small screens). User always knows where they are and can jump to any section without scrolling back up.
* Dashboard hero dialed down from the full marketing treatment to a workspace-sized header (clamp(24px,3vw,34px) instead of clamp(32px,5vw,58px), tighter padding, smaller reminder note). The dashboard is a transactional hub, not a landing page — content should dominate, not the hero.

V1.4.9 self-healing install + cv=0 hotfix:
* CRITICAL FIX — Share-link URLs were being generated with cv=0 when CV inserts silently failed. Root cause: users uploading a new zip via WP Plugins → Add New → Replace, without deactivating+reactivating the plugin, leave dbDelta stale; new columns added in 1.4.x (candidate_feedback, ai_narrative, ai_narrative_at) never get created, so $wpdb->insert returned 0 and the redirect URL contained ?cv=0. The user then clicked the email link and saw "page not found" (or a theme redirect to home).
* SSC_CH_Activator::maybe_upgrade() now runs on every admin_init. Compares the plugin's compiled SSC_CH_DB_VERSION constant against the stored ssc_ch_db_version option; on mismatch, re-runs the full activator (dbDelta picks up new columns idempotently). The activator now uses update_option for the version stamp instead of add_option so the value actually changes on subsequent runs.
* SSC_CH_Activator::ensure_pages() auto-creates the 6 required WordPress pages on activation if they don't already exist: /jobs/, /cv-optimizer/, /cv-report/, /my-dashboard/, /my-cvs/, /my-credits/. Each gets its matching shortcode as content. Resulting page URLs are stamped into ssc_ch_settings so the account nav + cv_report_url always resolve. Safe to re-run (checks by slug first).
* Upload handler now refuses to send a broken URL. If SSC_CH_DB::insert_cv returns 0, it logs the wpdb->last_error to abuse_events and redirects with a new save_failed error code instead of building cv=0. The cv-form template renders a clear "we could not save your CV; admin has been notified — please try again" message.

V1.4.8 post-submission flow + analyzing UX:
* Redesigned the post-submission state from a small text card into a proper transactional confirmation panel (Stripe / Linear / Wise pattern). Shows: tone-coded status icon (green check for scored, amber clock for queued), big headline, SSC-CV-#NNNN reference number, "saved for [email]" account context, 3-step visual timeline (Saved → Reviewed → Report ready) with pulsing active dot, primary actions (Open report / Copy share link), and 4 next-step tiles (My CVs / Dashboard / Browse jobs / Submit another).
* Form is now hidden when the page renders in submission state — the user knows the action completed and is given clear next moves instead of an empty form. "Submit another" tile reloads the bare URL to bring the form back.
* New "Scored by" badge on every result: blue "Auto-scored by SSC readiness engine" for free local submissions, red "Local engine score · AI review running" when credits-spent priority review fires the OpenRouter cron, amber "Queued for human reviewer" for manual-review fallback. Users always know what evaluated their CV.
* When AI review isn't active (no key configured OR no credits spent), the success panel now shows an explicit upgrade prompt with a direct top-up link, so users know AI review is an option they can unlock.
* New full-page "Analyzing your CV…" overlay during form submission — animated three-dot pulse, cycling status messages ("Reading your submission / Extracting key signals / Scoring readiness / Saving to your account"), backdrop blur. Pure UX feedback (no AJAX); disappears naturally when the browser navigates to the success page.
* CV Optimizer hero h2 dialed down from clamp(32px,5vw,58px) to clamp(26px,3.4vw,38px) — the page is transactional, not marketing, so a 58px header was overwhelming.

V1.4.7 logic + navigation pass:
* Fixed success-card after CV submission. The v1.4.3 guard required score>0 to show the "scored" card; a legitimate 0/100 was falling through to the "manual review" message and confusing users into thinking their CV wasn't saved. New guard tests for the presence of ?cv= and ?t= in the URL (which only the upload handler sets when scoring actually happened). All branches now explicitly say "saved to your account" so the user knows the data persisted regardless of which message displays.
* Fixed CV analyzer keyword matching. The v1.4.0 tight word-boundary regex required BOTH sides of a keyword to be non-alphanumeric, so "guards" (plural), "patrolling", "licensed", "supervisors" all failed to match — artificially deflating scores. New regex requires only the start-of-word boundary; matches morphological variants but still blocks substring false positives like "rearguard" matching "guard".
* Cities + Training filters now grouped by state inside their collapsible panels — "New York" sub-section above "New Jersey" sub-section, with small uppercase headers. State filter hides the entire opposite-state sub-section (previously it only hid individual chips, leaving an orphaned section title).
* New account-nav tab bar (Dashboard / Jobs / Submit CV / My CVs / Credits) rendered at the top of every member shortcode. One consistent place to navigate; current page gets a solid SSC-blue pill. Horizontally scrollable on mobile.
* Dashboard upgraded to a real account hub:
  - Quick Actions row: "Submit a new CV" (primary gradient), "Browse jobs", "Top up credits" — clickable tiles with icon + title + subtitle.
  - New "Recent CVs" cards grid below the score trend. Each card shows score with tone-coloured number (green/amber/red), status pill, role, date, plus [View report] and [Copy link] buttons. Pending/manual-review CVs show an "Awaiting manual review" note.
* New generic data-ssc-copy-link clipboard handler. Used by Recent-CVs cards and the post-submission success card. Native Clipboard API with hidden-textarea fallback; brief "✓ Copied" confirmation flash.

V1.4.6 CV form vertical stretch fix:
* The form card was rendering ~1200px tall with only ~370px of content, producing huge dead space between the wizard chips, the step heading, the fields, and the Continue button. Root cause: the parent .ssc-ch-cv-shell is a 2-column grid (form + aside) with default align-items:stretch, so the form was being stretched to match the taller aside (credit balance + 4 top-up cards). The form is itself display:grid, so it then distributed the extra height across its internal rows — pushing chips, legend, fields, and nav far apart.
* Fix: align-items:start on the parent shell (form sizes to its own content), plus align-self:start + align-content:start on the form (defensive, pack rows at top regardless of container height). Both gaps collapse to the intended 14px row gap.

V1.4.5 dashboard hero + KPI layout fix:
* The dashboard hero was rendering "My Security Career Dashboard" as four stacked words ("My / Security / Career / Dashboard") at giant size — root cause: the hero is a 2-column grid (minmax(0,1fr) 320px) but the template loose-leafed three direct children (kicker, h2, reminder p), so the h2 landed in the 320px-wide column and word-wrapped.
* Restructured dashboard.php to match the Jobs hero pattern — content wrapped in a <div> + new <aside class="ssc-ch-hero-card"> that holds the stats (tracked / applied / CV reviews / credits) and the pathway chips. Now they live INSIDE the dark hero where the existing white-on-dark styling actually works.
* Added defensive CSS: any hero without an aside/hero-card child collapses to a single column (fixes [ssc_my_credits], [ssc_my_cvs], and the logged-out dashboard, all of which have no aside).
* Forced kicker pill to sizeof-content with higher specificity in case a parent theme applies "p { width: 100% }".
* Saved/tracked jobs table got proper styling — uppercase muted column headers, hover state, status pill in SSC blue, "View" link in SSC blue instead of inheriting the theme's anchor red.
* Stats grid switched to 2x2 inside the hero aside so the 4th stat doesn't orphan.

V1.4.4 jobs-page filter strip visual fix:
* Filter labels (State track / Role type / Training) were rendering as white-on-white because the v1.4.0 chip styles assumed the filter strip was on the dark hero gradient (legacy 1.3.x layout). Rewrote the entire chip/label/collapse block for the actual light background — labels are now uppercase muted ink, chips are blue-ink-on-white pills with solid SSC blue when active, collapsible City/Training panels have a clean +/− toggle.
* Broke the legacy 6-column grid (.ssc-ch-filters at line 425) for chip-mode forms — the chip form is now a clean vertical flex column so State / City / Role / Training / Search-Clear stack naturally instead of sprawling across the row.
* Search/Clear actions row has a top border + proper button styling so it reads as the form's commit zone.
* Improved focus state on inputs/selects (3px blue outline + ink border).

V1.4.3 spacing + UX patch on the CV Optimizer form:
* Fixed ~150px of dead vertical space inside the wizard fieldset by resetting Chrome's default fieldset min-inline-size and the legend's implicit top anchoring. Wizard steps now sit flush below the progress chips.
* Tightened wizard progress chips bottom spacing (was 22+24+border, now 14+16+border).
* Empty submit-row container no longer takes vertical space on non-last wizard steps (hidden via data-wizard-last="0").
* Success card no longer renders "Your CV scored 0/100" when the URL has ?submitted=1 without a real score. Falls through to a generic "received" message unless cv, t, and a positive score are all present.

V1.4.2 adds secure OpenRouter integration:
* New SSC_CH_OpenRouter helper. The API key is read ONLY from a wp-config.php constant (SSC_CH_OPENROUTER_KEY) — never from wp_options, so it does not appear in DB backups (JetBackup → Google Drive in this hosting setup). Optional SSC_CH_OPENROUTER_MODEL constant overrides the default model (anthropic/claude-haiku-4.5). Optional SSC_CH_OPENROUTER_HOST for self-hosted gateways.
* When a candidate spends credits for an ultimate CV review (status → priority_review), a WP-cron event fires SSC_CH_OpenRouter::run_for_cv() which calls OpenRouter, stores the draft narrative in cv_reviews.ai_narrative, and logs token usage + cost in ssc_ch_ai_usage.
* Admin gate: the AI draft is NEVER auto-published. It appears as an admin-only details panel on the CV Reviews row with a "Use this as candidate feedback" button that copies the draft into the candidate_feedback textarea. The admin reviews, edits, and saves before anything reaches the candidate.
* Per-call cost cap (openrouter_cost_cap_usd setting) is enforced as a 100× daily ceiling. If today's cumulative AI cost exceeds the ceiling, requests are skipped and logged to abuse_events.
* Settings page shows "Configured via wp-config.php (…last4)" or "Not configured" — the key value itself is never echoed to the page.
* Prompt-injection defence: CV text is wrapped in explicit ===CV BEGINS=== / ===CV ENDS=== markers, system prompt fixes the role, output is capped at 600 tokens, and the model is instructed not to mention being AI.
* New columns on ssc_ch_cv_reviews: ai_narrative, ai_narrative_at. Existing CVs are unaffected.
* If the constant is absent, the plugin degrades to the prior behaviour (local heuristic + human-fulfilled priority reviews). No errors, no half-broken state.

V1.4.1 sanity-check pass on top of 1.4.0:
* CV form wizard is now progressive enhancement — without JS the form renders as a single page with a working submit button; with JS the multi-step nav + per-step validation kicks in.
* [ssc_my_cvs] cards now expose a "View report" button per scored CV (was missing the link in v1.4.0).
* Candidate confirmation email goes out automatically after CV submission, with the shareable report link and clear NY/NJ disclaimer.
* New candidate_feedback field on CV reviews — admins can write public-facing reviewer notes (rendered on the CV report) separately from internal admin_notes, and can tick "Email the candidate" to push the update.
* Candidate dashboard now includes a "CV score trend" bar chart of the last 5 reviews, with score-delta vs previous and a one-click link to the latest report.
* Chart.js admin enqueue keeps the CDN dependency but the placeholder SRI was removed (a wrong digest would brick the charts); doc-comment explains how to pin the real hash post-install.
* New DB columns on ssc_ch_cv_reviews: candidate_feedback, candidate_notified_at.

V1.4.0 was a major design + capability pass:
* Shareable token-based CV report at [ssc_cv_report] — full score breakdown, six dimension bars, strengths, next-step checklist, recommendation CTA, score-over-time delta, and OG/Twitter share-card metadata.
* Pure-PHP PDF and DOCX text extraction (no Composer deps, no shelling out) — text-based uploads now get scored automatically; only scanned/encrypted PDFs fall through to manual review.
* UTM/source attribution capture (cookie-based, sendBeacon ingest) joins to CV submissions and credit purchases, surfacing a "where candidates come from" widget on the analytics page.
* Admin analytics rebuilt: date-range filter with 7/30/90/all presets, Chart.js daily activity + credits-purchased charts, conversion funnel (saves → applied → CV → credits), CSV export of the credit ledger, and tone-coded ledger rows.
* Mobile CV form is now a 3-step wizard with progress chips and per-step validation.
* Mobile jobs filter swaps native multi-select for tap-friendly chip groups in collapsible details panels.
* Bug fixes: word-boundary keyword matching in the CV analyzer (no more "patrol" inside "patrol-style"), removed all three unused legacy CSS files, dropped legacy 0000-00-00 deleted_at fallbacks, hardened error reflection on the CV form.
* New DB tables: ssc_ch_attribution (UTM/referrer/device per session) and ssc_ch_report_views (CV report engagement). New columns on ssc_ch_cv_reviews: share_token, share_views, share_first_viewed_at, extraction_source, attribution_id. Existing CVs are backfilled with share tokens on upgrade.

V1.3.4 added admin manual credit adjustments, a fuller analytics control room, recent credit ledger visibility, CV status breakdowns, and stronger candidate dashboard stats. V1.3.3 tightened the CV/credit flow review: empty submissions are blocked, file-only CVs are queued for manual review, priority credit spend uses the logged-in account identity. V1.3.2 preserved manually uploaded WooCommerce product images. V1.3.1 fixed WooCommerce variation checkout links. V1.3.0 added credit top-ups, My Credits/My CVs shortcodes, CV scoring, safer PDF/DOCX/TXT uploads, credit analytics, and API-cost tracking.

== Description ==
SSC Career Hub provides shortcodes:
* [ssc_jobs] — searchable job directory from local DB synced from CSV/published Google Sheet.
* [ssc_job_dashboard] — simple seeker dashboard for saved/applied activity.
* [ssc_cv_optimizer] — three-step CV intake wizard with readiness score and admin review.
* [ssc_cv_report] — public token-shareable CV readiness report (place on its own page, e.g. /cv-report/).
* [ssc_my_credits] — logged-in candidate credit balance and smooth top-up cards.
* [ssc_my_cvs] — logged-in candidate CV review tracker.

Important: SSC provides training and career preparation support. Job listings are informational and employment is not guaranteed. New York DCJS training and New Jersey SORA are distinct requirements.

== Installation ==
Upload/activate the plugin, place the shortcodes on pages, configure CSV URL under SSC Career Hub > Sync Jobs, then run Manual Sync.

== CSV Columns ==
Supports common headers: id, title/job_title, company/employer, city/location, state, job_type/type, pay/pay_text, schedule, training_requirement/training, tags, summary/description, requirements, apply_url/url, source_name, source_url, verified_at, expires_at.

== Security ==
Uses nonces/capabilities, fixed file allowlist (txt/pdf/docx only; legacy .doc is blocked), private upload folder protection, hashed IP/email for rate/abuse events, formula-safe CSV helper, and WooCommerce buyer detection only when WooCommerce functions exist.
