{"product_id":"lw-course-catalog","title":"LW Course Catalog - WordPress plugin — display LearnWorlds courses on any page with a shortcode","description":"\u003cbody\u003e\n\n\n\u003cmeta charset=\"UTF-8\"\u003e\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n\u003ctitle\u003eLW Course Catalog — Plugin Documentation\u003c\/title\u003e\n\u003cstyle\u003e\n  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n  :root {\n    --brand:        #2563eb;\n    --brand-dark:   #1d4ed8;\n    --brand-light:  #eff6ff;\n    --brand-muted:  #bfdbfe;\n    --text:         #1e293b;\n    --text-muted:   #64748b;\n    --border:       #e2e8f0;\n    --bg:           #f8fafc;\n    --bg-white:     #ffffff;\n    --success-bg:   #f0fdf4;\n    --success-text: #166534;\n    --success-border:#bbf7d0;\n    --warn-bg:      #fffbeb;\n    --warn-text:    #92400e;\n    --warn-border:  #fde68a;\n    --info-bg:      #eff6ff;\n    --info-text:    #1e40af;\n    --info-border:  #bfdbfe;\n    --radius:       10px;\n    --radius-sm:    6px;\n    --shadow:       0 1px 3px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04);\n  }\n\n  html { scroll-behavior: smooth; }\n\n  body {\n    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;\n    font-size: 15px;\n    line-height: 1.7;\n    color: var(--text);\n    background: var(--bg);\n  }\n\n  \/* ── Layout ── *\/\n  .layout { display: flex; min-height: 100vh; }\n\n  .sidebar {\n    width: 260px;\n    flex-shrink: 0;\n    background: var(--bg-white);\n    border-right: 1px solid var(--border);\n    position: sticky;\n    top: 0;\n    height: 100vh;\n    overflow-y: auto;\n    padding: 2rem 1.25rem;\n  }\n\n  .main {\n    flex: 1;\n    max-width: 820px;\n    padding: 3rem 3rem 4rem;\n  }\n\n  @media (max-width: 768px) {\n    .layout { flex-direction: column; }\n    .sidebar { width: 100%; height: auto; position: static; border-right: none; border-bottom: 1px solid var(--border); }\n    .main { padding: 2rem 1.25rem 3rem; }\n  }\n\n  \/* ── Sidebar ── *\/\n  .sidebar-logo { display: flex; align-items: center; gap: 10px; margin-bottom: 2rem; }\n  .sidebar-logo-icon {\n    width: 36px; height: 36px; border-radius: 8px;\n    background: var(--brand); color: #fff;\n    display: flex; align-items: center; justify-content: center;\n    font-size: 18px;\n  }\n  .sidebar-logo-text { font-weight: 700; font-size: 14px; line-height: 1.3; color: var(--text); }\n  .sidebar-logo-sub { font-size: 11px; color: var(--text-muted); font-weight: 400; }\n\n  .nav-section { margin-bottom: 1.5rem; }\n  .nav-section-label {\n    font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;\n    color: var(--text-muted); margin-bottom: .5rem; padding-left: .5rem;\n  }\n  .nav-section ul { list-style: none; display: flex; flex-direction: column; gap: 2px; }\n  .nav-section ul li a {\n    display: block; padding: .4rem .75rem; border-radius: var(--radius-sm);\n    font-size: 13px; color: var(--text-muted); text-decoration: none;\n    transition: all .15s;\n  }\n  .nav-section ul li a:hover { background: var(--brand-light); color: var(--brand); }\n\n  .sidebar-version {\n    margin-top: 2rem; padding: .5rem .75rem; background: var(--bg); border-radius: var(--radius-sm);\n    font-size: 12px; color: var(--text-muted);\n  }\n  .sidebar-version span { color: var(--brand); font-weight: 600; }\n\n  \/* ── Header ── *\/\n  .doc-header {\n    margin-bottom: 3rem; padding-bottom: 2rem; border-bottom: 1px solid var(--border);\n  }\n  .doc-badge {\n    display: inline-block; font-size: 11px; font-weight: 600; padding: 3px 10px;\n    border-radius: 99px; background: var(--brand-light); color: var(--brand);\n    margin-bottom: .75rem; letter-spacing: .02em;\n  }\n  .doc-title { font-size: 32px; font-weight: 800; color: var(--text); line-height: 1.2; margin-bottom: .5rem; }\n  .doc-subtitle { font-size: 16px; color: var(--text-muted); line-height: 1.6; }\n\n  .meta-pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 1.25rem; }\n  .meta-pill {\n    display: flex; align-items: center; gap: 6px; font-size: 12px;\n    padding: 4px 10px; border-radius: 99px; background: var(--bg);\n    border: 1px solid var(--border); color: var(--text-muted);\n  }\n  .meta-pill strong { color: var(--text); }\n\n  \/* ── Sections ── *\/\n  .section { margin-bottom: 3.5rem; scroll-margin-top: 2rem; }\n  .section-title {\n    display: flex; align-items: center; gap: 10px;\n    font-size: 22px; font-weight: 700; color: var(--text);\n    margin-bottom: 1.25rem; padding-bottom: .75rem;\n    border-bottom: 2px solid var(--brand-light);\n  }\n  .section-title .s-icon {\n    width: 36px; height: 36px; border-radius: 8px;\n    background: var(--brand-light); color: var(--brand);\n    display: flex; align-items: center; justify-content: center;\n    font-size: 18px; flex-shrink: 0;\n  }\n\n  .subsection { margin-bottom: 1.75rem; }\n  .subsection-title { font-size: 16px; font-weight: 700; color: var(--text); margin-bottom: .75rem; }\n\n  p { color: var(--text-muted); margin-bottom: .9rem; }\n  p:last-child { margin-bottom: 0; }\n  p strong { color: var(--text); }\n\n  \/* ── Steps ── *\/\n  .steps { display: flex; flex-direction: column; gap: 0; margin-bottom: 1.25rem; }\n  .step { display: flex; gap: 0; }\n  .step-left { display: flex; flex-direction: column; align-items: center; width: 40px; flex-shrink: 0; }\n  .step-num {\n    width: 28px; height: 28px; border-radius: 50%; background: var(--brand);\n    color: #fff; font-size: 13px; font-weight: 700;\n    display: flex; align-items: center; justify-content: center; flex-shrink: 0;\n  }\n  .step-line { width: 2px; background: var(--brand-muted); flex: 1; margin: 4px 0; }\n  .step:last-child .step-line { display: none; }\n  .step-content { padding: 2px 0 20px 4px; }\n  .step-content strong { display: block; font-size: 14px; color: var(--text); font-weight: 600; margin-bottom: 2px; }\n  .step-content span { font-size: 13px; color: var(--text-muted); }\n\n  \/* ── Tables ── *\/\n  .table-wrap { overflow-x: auto; margin-bottom: 1.25rem; border-radius: var(--radius); border: 1px solid var(--border); }\n  table { width: 100%; border-collapse: collapse; font-size: 13px; }\n  thead th {\n    background: var(--bg); font-size: 11px; font-weight: 700;\n    text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);\n    padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border);\n  }\n  tbody td { padding: 10px 14px; border-bottom: 1px solid var(--border); vertical-align: top; color: var(--text-muted); line-height: 1.5; }\n  tbody tr:last-child td { border-bottom: none; }\n  tbody td:first-child { color: var(--text); font-weight: 500; white-space: nowrap; }\n  tbody tr:hover td { background: var(--bg); }\n\n  \/* ── Code ── *\/\n  code {\n    font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace;\n    font-size: 12.5px; background: var(--brand-light); color: var(--brand);\n    padding: 2px 7px; border-radius: 4px; border: 1px solid var(--brand-muted);\n  }\n  pre {\n    background: #0f172a; color: #e2e8f0;\n    padding: 1.25rem 1.5rem; border-radius: var(--radius);\n    font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 13px;\n    line-height: 1.8; overflow-x: auto; margin-bottom: 1.25rem;\n  }\n  pre code { background: none; color: #93c5fd; padding: 0; border: none; font-size: 13px; }\n  pre .comment { color: #64748b; }\n  pre .attr { color: #86efac; }\n  pre .val { color: #fde68a; }\n\n  \/* ── Notices ── *\/\n  .notice {\n    display: flex; gap: 10px; padding: .85rem 1rem;\n    border-radius: var(--radius-sm); margin-bottom: 1.25rem;\n    font-size: 13px; line-height: 1.6;\n  }\n  .notice .n-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }\n  .notice.tip { background: var(--success-bg); border-left: 3px solid var(--success-border); color: var(--success-text); }\n  .notice.warn { background: var(--warn-bg); border-left: 3px solid var(--warn-border); color: var(--warn-text); }\n  .notice.info { background: var(--info-bg); border-left: 3px solid var(--info-border); color: var(--info-text); }\n\n  \/* ── Card grid ── *\/\n  .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 1.25rem; }\n  .card-sm {\n    background: var(--bg-white); border: 1px solid var(--border);\n    border-radius: var(--radius); padding: 1rem 1.1rem; font-size: 13px;\n  }\n  .card-sm-title { font-weight: 600; color: var(--text); margin-bottom: 4px; font-size: 13px; }\n  .card-sm p { font-size: 12.5px; margin: 0; }\n\n  \/* ── Filter logic table ── *\/\n  .filter-table { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 1.25rem; }\n  .ft-row { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; align-items: center; padding: 9px 14px; border-bottom: 1px solid var(--border); gap: 8px; font-size: 12.5px; }\n  .ft-row.header { background: var(--bg); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); }\n  .ft-row:last-child { border-bottom: none; }\n  .ft-row:not(.header):hover { background: var(--bg); }\n  .ft-label { color: var(--text-muted); }\n  .pill {\n    display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px;\n    border-radius: 99px; white-space: nowrap;\n  }\n  .pill.hide { background: #fee2e2; color: #991b1b; }\n  .pill.show { background: #dcfce7; color: #166534; }\n  .pill.na { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); }\n  .ft-result { font-weight: 600; font-size: 12px; }\n  .ft-result.shown { color: #166534; }\n  .ft-result.hidden { color: #dc2626; }\n\n  \/* ── Shortcode examples ── *\/\n  .shortcode-ex { margin-bottom: 1rem; }\n  .sc-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-bottom: 6px; }\n\n  \/* ── Troubleshoot ── *\/\n  .trouble-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 1rem; }\n  .trouble-item {\n    background: var(--bg-white); border: 1px solid var(--border);\n    border-radius: var(--radius); padding: 1rem 1.1rem;\n  }\n  .trouble-q { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 5px; display: flex; gap: 8px; align-items: flex-start; }\n  .trouble-q .t-icon { color: var(--warn-text); flex-shrink: 0; margin-top: 1px; }\n  .trouble-a { font-size: 13px; color: var(--text-muted); padding-left: 22px; }\n\n  \/* ── Footer ── *\/\n  .doc-footer {\n    margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border);\n    font-size: 12px; color: var(--text-muted);\n    display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;\n  }\n  .doc-footer a { color: var(--brand); text-decoration: none; }\n\n  \/* Print *\/\n  @media print {\n    .sidebar { display: none; }\n    .main { padding: 1rem; max-width: 100%; }\n  }\n\u003c\/style\u003e\n\n\n\n\u003cdiv class=\"layout\"\u003e\n\n  \u003c!-- Sidebar --\u003e\n  \u003caside class=\"sidebar\"\u003e\n    \u003cdiv class=\"sidebar-logo\"\u003e\n      \u003cdiv class=\"sidebar-logo-icon\"\u003e📚\u003c\/div\u003e\n      \u003cdiv\u003e\n        \u003cdiv class=\"sidebar-logo-text\"\u003eLW Course Catalog\u003c\/div\u003e\n        \u003cdiv class=\"sidebar-logo-sub\"\u003ePlugin Documentation\u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003cnav\u003e\n      \u003cdiv class=\"nav-section\"\u003e\n        \u003cdiv class=\"nav-section-label\"\u003eGetting started\u003c\/div\u003e\n        \u003cul\u003e\n          \u003cli\u003e\u003ca href=\"#install\"\u003eInstallation\u003c\/a\u003e\u003c\/li\u003e\n          \u003cli\u003e\u003ca href=\"#credentials\"\u003eLearnWorlds credentials\u003c\/a\u003e\u003c\/li\u003e\n          \u003cli\u003e\u003ca href=\"#settings\"\u003ePlugin settings\u003c\/a\u003e\u003c\/li\u003e\n        \u003c\/ul\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"nav-section\"\u003e\n        \u003cdiv class=\"nav-section-label\"\u003eConfiguration\u003c\/div\u003e\n        \u003cul\u003e\n          \u003cli\u003e\u003ca href=\"#card\"\u003eCard design\u003c\/a\u003e\u003c\/li\u003e\n          \u003cli\u003e\u003ca href=\"#filtering\"\u003eDraft \u0026amp; private filtering\u003c\/a\u003e\u003c\/li\u003e\n          \u003cli\u003e\u003ca href=\"#pagination\"\u003ePagination\u003c\/a\u003e\u003c\/li\u003e\n        \u003c\/ul\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"nav-section\"\u003e\n        \u003cdiv class=\"nav-section-label\"\u003eReference\u003c\/div\u003e\n        \u003cul\u003e\n          \u003cli\u003e\u003ca href=\"#shortcode\"\u003eShortcode reference\u003c\/a\u003e\u003c\/li\u003e\n          \u003cli\u003e\u003ca href=\"#debug\"\u003eDebug log\u003c\/a\u003e\u003c\/li\u003e\n          \u003cli\u003e\u003ca href=\"#troubleshoot\"\u003eTroubleshooting\u003c\/a\u003e\u003c\/li\u003e\n        \u003c\/ul\u003e\n      \u003c\/div\u003e\n    \u003c\/nav\u003e\n\n    \u003cdiv class=\"sidebar-version\"\u003eVersion \u003cspan\u003e1.4.4\u003c\/span\u003e · Built by Codendesigner\u003c\/div\u003e\n  \u003c\/aside\u003e\n\n  \u003c!-- Main content --\u003e\n  \u003cmain class=\"main\"\u003e\n\n    \u003c!-- Header --\u003e\n    \u003cdiv class=\"doc-header\"\u003e\n      \u003cdiv class=\"doc-badge\"\u003eWordPress Plugin\u003c\/div\u003e\n      \u003ch1 class=\"doc-title\"\u003eLW Course Catalog\u003c\/h1\u003e\n      \u003cp class=\"doc-subtitle\"\u003eDisplay courses from one or two LearnWorlds schools on any WordPress page using a simple shortcode. Visitors browse on WordPress and are redirected to LearnWorlds to enroll and pay.\u003c\/p\u003e\n      \u003cdiv class=\"meta-pills\"\u003e\n        \u003cdiv class=\"meta-pill\"\u003e📦 \u003cstrong\u003ev1.4.4\u003c\/strong\u003e\n\u003c\/div\u003e\n        \u003cdiv class=\"meta-pill\"\u003e🔧 \u003cstrong\u003eWordPress 5.8+\u003c\/strong\u003e\n\u003c\/div\u003e\n        \u003cdiv class=\"meta-pill\"\u003e🐘 \u003cstrong\u003ePHP 7.4+\u003c\/strong\u003e\n\u003c\/div\u003e\n        \u003cdiv class=\"meta-pill\"\u003e🏫 \u003cstrong\u003eSupports 2 schools\u003c\/strong\u003e\n\u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003c!-- ── INSTALLATION ── --\u003e\n    \u003csection class=\"section\" id=\"install\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e⬇️\u003c\/span\u003e Installation\u003c\/h2\u003e\n      \u003cdiv class=\"steps\"\u003e\n        \u003cdiv class=\"step\"\u003e\n          \u003cdiv class=\"step-left\"\u003e\n\u003cdiv class=\"step-num\"\u003e1\u003c\/div\u003e\n\u003cdiv class=\"step-line\"\u003e\u003c\/div\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"step-content\"\u003e\n\u003cstrong\u003eUpload the plugin\u003c\/strong\u003e\u003cspan\u003eIn WordPress admin go to \u003cstrong\u003ePlugins → Add New → Upload Plugin\u003c\/strong\u003e\u003c\/span\u003e\n\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"step\"\u003e\n          \u003cdiv class=\"step-left\"\u003e\n\u003cdiv class=\"step-num\"\u003e2\u003c\/div\u003e\n\u003cdiv class=\"step-line\"\u003e\u003c\/div\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"step-content\"\u003e\n\u003cstrong\u003eSelect the zip file\u003c\/strong\u003e\u003cspan\u003eChoose \u003ccode\u003elw-course-catalog.zip\u003c\/code\u003e and click \u003cstrong\u003eInstall Now\u003c\/strong\u003e\u003c\/span\u003e\n\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"step\"\u003e\n          \u003cdiv class=\"step-left\"\u003e\n\u003cdiv class=\"step-num\"\u003e3\u003c\/div\u003e\n\u003cdiv class=\"step-line\"\u003e\u003c\/div\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"step-content\"\u003e\n\u003cstrong\u003eActivate\u003c\/strong\u003e\u003cspan\u003eClick \u003cstrong\u003eActivate Plugin\u003c\/strong\u003e after installation completes\u003c\/span\u003e\n\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"step\"\u003e\n          \u003cdiv class=\"step-left\"\u003e\n\u003cdiv class=\"step-num\"\u003e4\u003c\/div\u003e\n\u003cdiv class=\"step-line\"\u003e\u003c\/div\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"step-content\"\u003e\n\u003cstrong\u003eOpen settings\u003c\/strong\u003e\u003cspan\u003eGo to \u003cstrong\u003eSettings → LW Course Catalog\u003c\/strong\u003e to configure your credentials\u003c\/span\u003e\n\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n      \u003cdiv class=\"notice tip\"\u003e\n\u003cspan class=\"n-icon\"\u003e✅\u003c\/span\u003e No other plugins are required. The plugin is self-contained with its own caching, logging, and admin UI.\u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── CREDENTIALS ── --\u003e\n    \u003csection class=\"section\" id=\"credentials\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e🔑\u003c\/span\u003e Getting your LearnWorlds credentials\u003c\/h2\u003e\n      \u003cp\u003eAll three values you need are on one page inside your LearnWorlds school admin. Navigate to:\u003c\/p\u003e\n      \u003cpre\u003e\u003ccode\u003ehttps:\/\/yourschool.com\/author\/settings_api\u003c\/code\u003e\u003c\/pre\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eWhat to copy\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eValue\u003c\/th\u003e\n\u003cth\u003eWhere to find it\u003c\/th\u003e\n\u003cth\u003eNotes\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eAPI URL\u003c\/td\u003e\n\u003ctd\u003eThe \"API URL\" field with a Copy button\u003c\/td\u003e\n\u003ctd\u003eLooks like \u003ccode\u003ehttps:\/\/yourschool.com\/admin\/api\/\u003c\/code\u003e — copy it exactly as shown\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eClient ID\u003c\/td\u003e\n\u003ctd\u003eClick the eye 👁 icon to reveal, then Copy\u003c\/td\u003e\n\u003ctd\u003eRequired alongside the Access Token\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eAccess Token\u003c\/td\u003e\n\u003ctd\u003eUnder \"Access Tokens\" section → click Create → eye icon → copy\u003c\/td\u003e\n\u003ctd\u003eRecommended auth method. Long-lived, no expiry unless revoked.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eClient Secret\u003c\/td\u003e\n\u003ctd\u003eClick Copy next to the masked field\u003c\/td\u003e\n\u003ctd\u003eOptional — only needed if not using an Access Token\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"notice info\"\u003e\n\u003cspan class=\"n-icon\"\u003eℹ️\u003c\/span\u003e The \u003cstrong\u003eAPI URL must include the full path\u003c\/strong\u003e (\u003ccode\u003e\/admin\/api\/\u003c\/code\u003e). Do not use just the root domain — the plugin uses the exact URL you provide to build all API calls.\u003c\/div\u003e\n      \u003cdiv class=\"notice warn\"\u003e\n\u003cspan class=\"n-icon\"\u003e⚠️\u003c\/span\u003e If your school uses a custom domain (e.g. \u003ccode\u003ewww.foodsurety.com\u003c\/code\u003e), use that custom domain in the API URL — not the \u003ccode\u003e.learnworlds.com\u003c\/code\u003e subdomain.\u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── SETTINGS ── --\u003e\n    \u003csection class=\"section\" id=\"settings\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e⚙️\u003c\/span\u003e Plugin settings\u003c\/h2\u003e\n      \u003cp\u003eGo to \u003cstrong\u003eSettings → LW Course Catalog → ⚙️ Settings tab\u003c\/strong\u003e. There are two school sections — School A is required, School B is optional.\u003c\/p\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eSchool A \/ School B fields\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eField\u003c\/th\u003e\n\u003cth\u003eRequired\u003c\/th\u003e\n\u003cth\u003eDescription\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eAPI URL\u003c\/td\u003e\n\u003ctd\u003eYes\u003c\/td\u003e\n\u003ctd\u003eFull API URL from LearnWorlds settings. Example: \u003ccode\u003ehttps:\/\/yourschool.com\/admin\/api\/\u003c\/code\u003e\n\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eAccess Token ⭐\u003c\/td\u003e\n\u003ctd\u003eRecommended\u003c\/td\u003e\n\u003ctd\u003eLong-lived token. Paste the full string. If provided, Client Secret is not needed.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eClient ID\u003c\/td\u003e\n\u003ctd\u003eYes\u003c\/td\u003e\n\u003ctd\u003eRequired alongside Access Token for the \u003ccode\u003eLw-Client\u003c\/code\u003e request header.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eClient Secret\u003c\/td\u003e\n\u003ctd\u003eOptional\u003c\/td\u003e\n\u003ctd\u003eOnly used if no Access Token is provided. Plugin falls back to OAuth2 client credentials flow.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eCache settings\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eField\u003c\/th\u003e\n\u003cth\u003eDefault\u003c\/th\u003e\n\u003cth\u003eDescription\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eCache Duration\u003c\/td\u003e\n\u003ctd\u003e12 hours\u003c\/td\u003e\n\u003ctd\u003eHow often course data is re-fetched from LearnWorlds. Options: 1h, 3h, 6h, 12h, 24h. Shorter = fresher data, more API calls. Longer = faster page loads.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"notice info\"\u003e\n\u003cspan class=\"n-icon\"\u003eℹ️\u003c\/span\u003e Leave School B fields completely blank until your second school is ready. It will be automatically included once configured. The plugin will not show errors for an empty School B.\u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eStatus tab\u003c\/div\u003e\n        \u003cp\u003eThe \u003cstrong\u003e📊 Status tab\u003c\/strong\u003e shows the cache state for each school (how many minutes remain before the next auto-refresh), the current configured URLs, and two action buttons:\u003c\/p\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eButton\u003c\/th\u003e\n\u003cth\u003eWhat it does\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eRefresh Cache Now\u003c\/td\u003e\n\u003ctd\u003eImmediately clears the cache and re-fetches all courses from all configured schools\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eTest Connection \u0026amp; Re-fetch\u003c\/td\u003e\n\u003ctd\u003eSame as above but also redirects you to the Debug Log tab to see the full fetch result\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── CARD DESIGN ── --\u003e\n    \u003csection class=\"section\" id=\"card\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e🎨\u003c\/span\u003e Card design settings\u003c\/h2\u003e\n      \u003cp\u003eGo to \u003cstrong\u003eSettings → LW Course Catalog → 🎨 Card Design tab\u003c\/strong\u003e. All settings apply globally to every course card on the site.\u003c\/p\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eDisplay toggles\u003c\/div\u003e\n        \u003cdiv class=\"card-grid\"\u003e\n          \u003cdiv class=\"card-sm\"\u003e\n\u003cdiv class=\"card-sm-title\"\u003eShow thumbnail\u003c\/div\u003e\n\u003cp\u003eToggle the course image on or off\u003c\/p\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"card-sm\"\u003e\n\u003cdiv class=\"card-sm-title\"\u003eShow description\u003c\/div\u003e\n\u003cp\u003eToggle the excerpt text on or off\u003c\/p\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"card-sm\"\u003e\n\u003cdiv class=\"card-sm-title\"\u003eShow price\u003c\/div\u003e\n\u003cp\u003eToggle the price badge on or off\u003c\/p\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"card-sm\"\u003e\n\u003cdiv class=\"card-sm-title\"\u003eShow categories\u003c\/div\u003e\n\u003cp\u003eToggle category badges on or off\u003c\/p\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"card-sm\"\u003e\n\u003cdiv class=\"card-sm-title\"\u003eShow Enroll button\u003c\/div\u003e\n\u003cp\u003eLinks directly to the payment\/checkout page\u003c\/p\u003e\n\u003c\/div\u003e\n          \u003cdiv class=\"card-sm\"\u003e\n\u003cdiv class=\"card-sm-title\"\u003eShow Learn More button\u003c\/div\u003e\n\u003cp\u003eLinks to the course detail\/landing page\u003c\/p\u003e\n\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eCourse visibility\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eSetting\u003c\/th\u003e\n\u003cth\u003eDefault\u003c\/th\u003e\n\u003cth\u003eDescription\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eHide draft courses\u003c\/td\u003e\n\u003ctd\u003eOn ✅\u003c\/td\u003e\n\u003ctd\u003eHides courses with \u003ccode\u003estatus: draft\u003c\/code\u003e. Has absolute priority — a draft course is always hidden when this is on, regardless of the private setting.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eHide private courses\u003c\/td\u003e\n\u003ctd\u003eOn ✅\u003c\/td\u003e\n\u003ctd\u003eHides courses with \u003ccode\u003eaccess: private\u003c\/code\u003e. Only evaluated after the draft check passes.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"notice tip\"\u003e\n\u003cspan class=\"n-icon\"\u003e✅\u003c\/span\u003e When you save Card Design settings, the cache is automatically cleared. The change takes effect on the next page load — no manual refresh needed.\u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eText \u0026amp; layout\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eSetting\u003c\/th\u003e\n\u003cth\u003eDefault\u003c\/th\u003e\n\u003cth\u003eDescription\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eEnroll button text\u003c\/td\u003e\n\u003ctd\u003eEnroll Now\u003c\/td\u003e\n\u003ctd\u003eLabel on the primary CTA button. Can also be overridden per-shortcode.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eLearn More button text\u003c\/td\u003e\n\u003ctd\u003eLearn More\u003c\/td\u003e\n\u003ctd\u003eLabel on the secondary button. Can also be overridden per-shortcode.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eDescription length\u003c\/td\u003e\n\u003ctd\u003e20 words\u003c\/td\u003e\n\u003ctd\u003eHow many words to show in the course description excerpt (5–100).\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eCourses per page\u003c\/td\u003e\n\u003ctd\u003e12\u003c\/td\u003e\n\u003ctd\u003eHow many cards to show before Prev\/Next pagination appears. Can be overridden per-shortcode with the \u003ccode\u003eper_page\u003c\/code\u003e attribute.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eColours\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eSetting\u003c\/th\u003e\n\u003cth\u003eDefault\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eCard background\u003c\/td\u003e\n\u003ctd\u003e\u003ccode\u003e#ffffff\u003c\/code\u003e\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eEnroll button background\u003c\/td\u003e\n\u003ctd\u003e\u003ccode\u003e#2563eb\u003c\/code\u003e\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eEnroll button text colour\u003c\/td\u003e\n\u003ctd\u003e\u003ccode\u003e#ffffff\u003c\/code\u003e\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eLearn More button background\u003c\/td\u003e\n\u003ctd\u003e\u003ccode\u003e#f1f5f9\u003c\/code\u003e\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eLearn More button text colour\u003c\/td\u003e\n\u003ctd\u003e\u003ccode\u003e#1e293b\u003c\/code\u003e\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── FILTERING ── --\u003e\n    \u003csection class=\"section\" id=\"filtering\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e🔍\u003c\/span\u003e Draft \u0026amp; private filtering\u003c\/h2\u003e\n      \u003cp\u003eThe plugin uses \u003cstrong\u003eOption C — Draft takes absolute priority\u003c\/strong\u003e. A course goes through two gates in strict order. If it is rejected at gate 1 (draft), it never reaches gate 2 (private).\u003c\/p\u003e\n\n      \u003cdiv class=\"filter-table\"\u003e\n        \u003cdiv class=\"ft-row header\"\u003e\n          \u003cdiv\u003eCourse status + access\u003c\/div\u003e\n          \u003cdiv\u003eHide draft setting\u003c\/div\u003e\n          \u003cdiv\u003eHide private setting\u003c\/div\u003e\n          \u003cdiv\u003eResult\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"ft-row\"\u003e\n          \u003cdiv class=\"ft-label\"\u003ePublished + Public\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill na\"\u003eeither\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill na\"\u003eeither\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv class=\"ft-result shown\"\u003e✅ Always shown\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"ft-row\"\u003e\n          \u003cdiv class=\"ft-label\"\u003ePublished + Private\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill na\"\u003eeither\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill hide\"\u003eON\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv class=\"ft-result hidden\"\u003e❌ Hidden\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"ft-row\"\u003e\n          \u003cdiv class=\"ft-label\"\u003ePublished + Private\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill na\"\u003eeither\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill show\"\u003eOFF\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv class=\"ft-result shown\"\u003e✅ Shown\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"ft-row\"\u003e\n          \u003cdiv class=\"ft-label\"\u003eDraft + Public\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill hide\"\u003eON\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill na\"\u003eirrelevant\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv class=\"ft-result hidden\"\u003e❌ Hidden (draft wins)\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"ft-row\"\u003e\n          \u003cdiv class=\"ft-label\"\u003eDraft + Private\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill hide\"\u003eON\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill na\"\u003eirrelevant\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv class=\"ft-result hidden\"\u003e❌ Hidden (draft wins)\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"ft-row\"\u003e\n          \u003cdiv class=\"ft-label\"\u003eDraft + Private\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill show\"\u003eOFF\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill hide\"\u003eON\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv class=\"ft-result hidden\"\u003e❌ Hidden by private rule\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"ft-row\"\u003e\n          \u003cdiv class=\"ft-label\"\u003eDraft + Private\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill show\"\u003eOFF\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv\u003e\u003cspan class=\"pill show\"\u003eOFF\u003c\/span\u003e\u003c\/div\u003e\n          \u003cdiv class=\"ft-result shown\"\u003e✅ Shown\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"notice info\"\u003e\n\u003cspan class=\"n-icon\"\u003eℹ️\u003c\/span\u003e The key principle: \u003cstrong\u003eDraft means \"not ready for anyone\"\u003c\/strong\u003e. Private means \"ready but restricted\". If Hide Draft is on, draft status overrides everything — even if private courses are being shown.\u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── PAGINATION ── --\u003e\n    \u003csection class=\"section\" id=\"pagination\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e📄\u003c\/span\u003e Pagination\u003c\/h2\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eAPI-side fetch (all courses)\u003c\/div\u003e\n        \u003cp\u003eOn first load the plugin fetches every page of courses from LearnWorlds automatically. LearnWorlds returns \u003cstrong\u003e50 courses per API page\u003c\/strong\u003e. For a school with 424 courses this means \u003cstrong\u003e9 API requests\u003c\/strong\u003e, all made server-side and cached.\u003c\/p\u003e\n        \u003cp\u003ePagination uses the \u003ccode\u003emeta.page\u003c\/code\u003e and \u003ccode\u003emeta.totalPages\u003c\/code\u003e fields from the LearnWorlds API response. The plugin keeps fetching until \u003ccode\u003epage === totalPages\u003c\/code\u003e.\u003c\/p\u003e\n        \u003cdiv class=\"notice warn\"\u003e\n\u003cspan class=\"n-icon\"\u003e⚠️\u003c\/span\u003e The first cache build on a large school (400+ courses) takes 15–40 seconds. All subsequent page loads for every visitor are served from cache instantly.\u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eDisplay-side pagination (Prev\/Next)\u003c\/div\u003e\n        \u003cp\u003eAfter all courses are fetched and cached, they are displayed in pages controlled by the \u003cstrong\u003eCourses per page\u003c\/strong\u003e setting (default: 12). Prev\/Next buttons appear automatically below the grid when the total exceeds the per-page limit.\u003c\/p\u003e\n        \u003cp\u003eThe category filter bar and pagination work together — filtering resets to page 1 and the page count updates to reflect the filtered total.\u003c\/p\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eCache lifecycle\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eEvent\u003c\/th\u003e\n\u003cth\u003eCache behaviour\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eFirst page load after activation\u003c\/td\u003e\n\u003ctd\u003eCache is empty — plugin fetches all courses and stores them\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eSubsequent page loads\u003c\/td\u003e\n\u003ctd\u003eServed from cache instantly — no API calls\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eCache TTL expires (default 12h)\u003c\/td\u003e\n\u003ctd\u003eWP-Cron automatically refreshes in the background\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eSettings saved\u003c\/td\u003e\n\u003ctd\u003eCache is busted immediately — fresh fetch on next load\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eCard Design settings saved\u003c\/td\u003e\n\u003ctd\u003eCache is busted — draft\/private filter changes take effect\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eManual Refresh Cache Now\u003c\/td\u003e\n\u003ctd\u003eImmediate bust and re-fetch triggered from admin\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── SHORTCODE ── --\u003e\n    \u003csection class=\"section\" id=\"shortcode\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e🧩\u003c\/span\u003e Shortcode reference\u003c\/h2\u003e\n      \u003cp\u003ePlace \u003ccode\u003e[lw_course_catalog]\u003c\/code\u003e on any WordPress page, post, or widget area. All attributes are optional — defaults come from the Card Design settings.\u003c\/p\u003e\n\n      \u003cdiv class=\"table-wrap\"\u003e\n        \u003ctable\u003e\n          \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eAttribute\u003c\/th\u003e\n\u003cth\u003eValues\u003c\/th\u003e\n\u003cth\u003eDefault\u003c\/th\u003e\n\u003cth\u003eDescription\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n          \u003ctbody\u003e\n            \u003ctr\u003e\n\u003ctd\u003eschool\u003c\/td\u003e\n\u003ctd\u003e\n\u003ccode\u003eboth\u003c\/code\u003e \/ \u003ccode\u003ea\u003c\/code\u003e \/ \u003ccode\u003eb\u003c\/code\u003e\n\u003c\/td\u003e\n\u003ctd\u003e\u003ccode\u003eboth\u003c\/code\u003e\u003c\/td\u003e\n\u003ctd\u003eWhich school's courses to display\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003ctr\u003e\n\u003ctd\u003ecolumns\u003c\/td\u003e\n\u003ctd\u003e\n\u003ccode\u003e1\u003c\/code\u003e – \u003ccode\u003e4\u003c\/code\u003e\n\u003c\/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c\/code\u003e\u003c\/td\u003e\n\u003ctd\u003eGrid column count. Automatically collapses to 2 on tablet, 1 on mobile.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003ctr\u003e\n\u003ctd\u003eper_page\u003c\/td\u003e\n\u003ctd\u003eAny number\u003c\/td\u003e\n\u003ctd\u003eFrom settings\u003c\/td\u003e\n\u003ctd\u003eCourses per page before pagination. Overrides the Card Design setting for this instance only.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003ctr\u003e\n\u003ctd\u003ecategory\u003c\/td\u003e\n\u003ctd\u003eCategory name\u003c\/td\u003e\n\u003ctd\u003e—\u003c\/td\u003e\n\u003ctd\u003ePre-filter to a single category. Example: \u003ccode\u003ecategory=\"Food Safety\"\u003c\/code\u003e\n\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003ctr\u003e\n\u003ctd\u003eenroll_text\u003c\/td\u003e\n\u003ctd\u003eAny text\u003c\/td\u003e\n\u003ctd\u003eFrom settings\u003c\/td\u003e\n\u003ctd\u003eOverride the Enroll button label for this shortcode instance\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003ctr\u003e\n\u003ctd\u003emore_text\u003c\/td\u003e\n\u003ctd\u003eAny text\u003c\/td\u003e\n\u003ctd\u003eFrom settings\u003c\/td\u003e\n\u003ctd\u003eOverride the Learn More button label for this shortcode instance\u003c\/td\u003e\n\u003c\/tr\u003e\n          \u003c\/tbody\u003e\n        \u003c\/table\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eExamples\u003c\/div\u003e\n\n        \u003cdiv class=\"shortcode-ex\"\u003e\n          \u003cdiv class=\"sc-label\"\u003eBasic — all courses from both schools\u003c\/div\u003e\n          \u003cpre\u003e\u003ccode\u003e[lw_course_catalog]\u003c\/code\u003e\u003c\/pre\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv class=\"shortcode-ex\"\u003e\n          \u003cdiv class=\"sc-label\"\u003eSchool A only, 2-column grid\u003c\/div\u003e\n          \u003cpre\u003e\u003ccode\u003e[lw_course_catalog school=\"a\" columns=\"2\"]\u003c\/code\u003e\u003c\/pre\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv class=\"shortcode-ex\"\u003e\n          \u003cdiv class=\"sc-label\"\u003eFilter to a specific category, 6 per page\u003c\/div\u003e\n          \u003cpre\u003e\u003ccode\u003e[lw_course_catalog category=\"Food Safety\" per_page=\"6\"]\u003c\/code\u003e\u003c\/pre\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv class=\"shortcode-ex\"\u003e\n          \u003cdiv class=\"sc-label\"\u003eCustom button text\u003c\/div\u003e\n          \u003cpre\u003e\u003ccode\u003e[lw_course_catalog enroll_text=\"Buy Now\" more_text=\"View Course\"]\u003c\/code\u003e\u003c\/pre\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv class=\"shortcode-ex\"\u003e\n          \u003cdiv class=\"sc-label\"\u003eFull example combining multiple attributes\u003c\/div\u003e\n          \u003cpre\u003e\u003ccode\u003e[lw_course_catalog school=\"a\" columns=\"3\" per_page=\"9\" category=\"HACCP\" enroll_text=\"Enroll Today\"]\u003c\/code\u003e\u003c\/pre\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eButton destinations\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eButton\u003c\/th\u003e\n\u003cth\u003eDestination\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eLearn More\u003c\/td\u003e\n\u003ctd\u003eThe course detail\/landing page on LearnWorlds — e.g. \u003ccode\u003ehttps:\/\/yourschool.com\/course\/intro-haccp\u003c\/code\u003e\n\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eEnroll Now\u003c\/td\u003e\n\u003ctd\u003eThe payment page on LearnWorlds — e.g. \u003ccode\u003ehttps:\/\/yourschool.com\/payment?product_id=intro-haccp\u0026amp;type=course\u003c\/code\u003e\n\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n        \u003cp\u003eBoth buttons open in a new tab. No payment or registration happens on WordPress — everything is handled by LearnWorlds.\u003c\/p\u003e\n      \u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── DEBUG LOG ── --\u003e\n    \u003csection class=\"section\" id=\"debug\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e🔎\u003c\/span\u003e Debug log\u003c\/h2\u003e\n      \u003cp\u003eGo to \u003cstrong\u003eSettings → LW Course Catalog → 🔍 Debug Log tab\u003c\/strong\u003e. The log records every step of the API fetch process — authentication, pagination, filtering, and caching.\u003c\/p\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eLog levels\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eLevel\u003c\/th\u003e\n\u003cth\u003eColour\u003c\/th\u003e\n\u003cth\u003eWhat it means\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eERROR\u003c\/td\u003e\n\u003ctd\u003e🔴 Red\u003c\/td\u003e\n\u003ctd\u003eSomething failed — authentication error, API error, or network problem. Always investigate these.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eWARN\u003c\/td\u003e\n\u003ctd\u003e🟡 Yellow\u003c\/td\u003e\n\u003ctd\u003eA non-fatal issue — e.g. School B not configured, or a fallback was used.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eINFO\u003c\/td\u003e\n\u003ctd\u003e🔵 Blue\u003c\/td\u003e\n\u003ctd\u003eNormal progress messages — token obtained, page fetched, courses cached.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eDEBUG\u003c\/td\u003e\n\u003ctd\u003e⚫ Gray\u003c\/td\u003e\n\u003ctd\u003eDetailed technical data — exact URLs called, raw API meta, field names. Useful for diagnosing image or pagination issues.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eActions\u003c\/div\u003e\n        \u003cdiv class=\"table-wrap\"\u003e\n          \u003ctable\u003e\n            \u003cthead\u003e\u003ctr\u003e\n\u003cth\u003eAction\u003c\/th\u003e\n\u003cth\u003eDescription\u003c\/th\u003e\n\u003c\/tr\u003e\u003c\/thead\u003e\n            \u003ctbody\u003e\n              \u003ctr\u003e\n\u003ctd\u003eRun Connection Test\u003c\/td\u003e\n\u003ctd\u003eClears the cache and triggers a fresh fetch from all configured schools. All steps are logged in real time. Use this after changing credentials.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eClear Log\u003c\/td\u003e\n\u003ctd\u003eRemoves all log entries. The log automatically keeps the most recent 200 entries.\u003c\/td\u003e\n\u003c\/tr\u003e\n              \u003ctr\u003e\n\u003ctd\u003eFilter buttons\u003c\/td\u003e\n\u003ctd\u003eShow only Errors, Warnings, Info, or Debug entries. Useful for quickly spotting problems.\u003c\/td\u003e\n\u003c\/tr\u003e\n            \u003c\/tbody\u003e\n          \u003c\/table\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"subsection\"\u003e\n        \u003cdiv class=\"subsection-title\"\u003eReading a successful fetch log\u003c\/div\u003e\n        \u003cp\u003eAfter a successful connection test you should see entries like this (newest first in the log):\u003c\/p\u003e\n        \u003cpre\u003e\u003cspan class=\"attr\"\u003eINFO\u003c\/span\u003e  [School A] ✅ Fetch complete. Total courses: 412 across 9 pages\n\u003cspan class=\"attr\"\u003eINFO\u003c\/span\u003e  [School A] Page 9 done: 24 items, 4 skipped, has_more=NO, total=412\n\u003cspan class=\"attr\"\u003eINFO\u003c\/span\u003e  [School A] Page 8 done: 50 items, 6 skipped, has_more=YES, total=388\n...\n\u003cspan class=\"attr\"\u003eINFO\u003c\/span\u003e  [School A] Page 1 done: 50 items, 10 skipped, has_more=YES, total=40\n\u003cspan class=\"attr\"\u003eINFO\u003c\/span\u003e  [School A] Using static access token\n\u003cspan class=\"attr\"\u003eINFO\u003c\/span\u003e  [School A] Starting API fetch\u003c\/pre\u003e\n      \u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- ── TROUBLESHOOTING ── --\u003e\n    \u003csection class=\"section\" id=\"troubleshoot\"\u003e\n      \u003ch2 class=\"section-title\"\u003e\n\u003cspan class=\"s-icon\"\u003e🛠️\u003c\/span\u003e Troubleshooting\u003c\/h2\u003e\n\n      \u003cdiv class=\"trouble-list\"\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e No courses shown at all\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eCheck the Settings tab — ensure the API URL, Client ID, and Access Token are all filled in correctly. Then go to the Debug Log tab and click \u003cstrong\u003eRun Connection Test\u003c\/strong\u003e. Read the ERROR entries to see exactly what failed.\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e Only 40–50 courses showing instead of 400+\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eThis was a pagination bug fixed in v1.4.2. Update to v1.4.4 and run a Connection Test. The log should show \"Fetch complete. Total courses: 400+\" across multiple pages.\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e Page 2 of the catalog is empty\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eThis was a display pagination bug fixed in v1.4.0. Update to v1.4.4. The JS now controls all card visibility — no conflict with server-side class names.\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e Auth failed — HTTP 404 error\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eThe API URL is incorrect. It must include the full path: \u003ccode\u003ehttps:\/\/yourschool.com\/admin\/api\/\u003c\/code\u003e. Do not use the root domain alone. Copy it exactly from LearnWorlds Settings → Developers → API.\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e Course images not showing\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eCheck the Debug Log → expand the \"Raw page 1 response\" DEBUG entry → look at the \"first_course\" field — it lists all field names in the API response. The plugin tries 15 image field name variants. If none match, let us know the exact field name shown and it can be added.\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e Draft or private courses still appearing\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eGo to Card Design tab, check the relevant hide option, and save. The cache auto-clears on save. If still showing, run a manual Connection Test to force a full re-fetch with the new filter applied.\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e Enroll button going to the wrong page\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eThe enroll URL is built from the course slug and package ID returned by the API. Check the Debug Log for the raw course data to verify the slug. If LearnWorlds provides a \u003ccode\u003epayment_url\u003c\/code\u003e field directly, the plugin uses that instead.\u003c\/div\u003e\n        \u003c\/div\u003e\n        \u003cdiv class=\"trouble-item\"\u003e\n          \u003cdiv class=\"trouble-q\"\u003e\n\u003cspan class=\"t-icon\"\u003e⚠️\u003c\/span\u003e Settings not saving\u003c\/div\u003e\n          \u003cdiv class=\"trouble-a\"\u003eYour WordPress user must have the \u003ccode\u003emanage_options\u003c\/code\u003e capability (Administrator role). Lower-level roles cannot save plugin settings.\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"notice info\"\u003e\n\u003cspan class=\"n-icon\"\u003eℹ️\u003c\/span\u003e For persistent issues, share the full Debug Log (especially ERROR and DEBUG entries) with your developer. The log contains the exact API responses needed to diagnose any problem.\u003c\/div\u003e\n    \u003c\/section\u003e\n\n    \u003c!-- Footer --\u003e\n    \u003cdiv class=\"doc-footer\"\u003e\n      \u003cdiv\u003eLW Course Catalog v1.4.4 · Built by \u003ca href=\"https:\/\/codendesigner.com\"\u003eCodendesigner\u003c\/a\u003e\n\u003c\/div\u003e\n      \u003cdiv\u003eWordPress Plugin · LearnWorlds Integration\u003c\/div\u003e\n    \u003c\/div\u003e\n\n  \u003c\/main\u003e\n\u003c\/div\u003e\n\n\n\u003c\/body\u003e","brand":"Codendesigner","offers":[{"title":"Default Title","offer_id":45422337949743,"sku":null,"price":299.0,"currency_code":"USD","in_stock":true}],"url":"https:\/\/codendesigner.com\/products\/lw-course-catalog","provider":"Codendesigner","version":"1.0","type":"link"}