<?php


$tphCtx = [
    'menu' => $activeMenu ?? '',
    'tab'  => $_GET['tab'] ?? '',
    'sub'  => $_GET['sub'] ?? '',
];
?>
<style>
#tphelp, #tphelp * { box-sizing: border-box; }
#tphelp { font: 14px/1.55 -apple-system, "Segoe UI", Roboto, system-ui, sans-serif; }

 
#tphelp-bubble {
    position: fixed; right: 22px; bottom: 22px; z-index: 99979;
    display: none; align-items: center; gap: 8px;
    padding: 11px 16px 11px 13px; border: 1px solid #e2e8f0; cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none;
    background: #fff; color: #1e293b; border-radius: 999px;
    font: 600 14px/1 -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
    box-shadow: 0 8px 22px -6px rgba(15,23,42,.22), 0 2px 6px rgba(15,23,42,.08);
    transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
    white-space: nowrap;
}
#tphelp-bubble:hover { transform: translateY(-2px); border-color: #cdd7e5; box-shadow: 0 12px 26px -6px rgba(15,23,42,.26); }
#tphelp-bubble.tphelp-dragging, #tphelp-bubble.tphelp-dragging:hover { cursor: grabbing; transition: none !important; transform: none !important; box-shadow: 0 16px 34px -8px rgba(15,23,42,.34) !important; border-color: #b9c4d4 !important; }
#tphelp-bubble i { font-size: 16px; color: #2563eb; }
#tphelp.open #tphelp-bubble { opacity: 0; pointer-events: none; }
 
#tpsup-bubble { display: none !important; }

 
#tphelp-backdrop {
    position: fixed; inset: 0; z-index: 99984; background: rgba(15,23,42,.34);
    opacity: 0; pointer-events: none; transition: opacity .25s ease;
    backdrop-filter: blur(1px);
}
#tphelp.open #tphelp-backdrop { opacity: 1; pointer-events: auto; }

 
#tphelp-drawer {
    position: fixed; top: 0; right: 0; bottom: 0; z-index: 99985;
    width: 432px; max-width: 100vw; background: #f6f8fb;
    display: flex; flex-direction: column; overflow-x: hidden;
    box-shadow: -24px 0 60px -20px rgba(15,23,42,.4);
    transform: translateX(101%); transition: transform .28s cubic-bezier(.4,0,.2,1);
    color: #0f172a;
}
#tphelp:not(.open) #tphelp-drawer { visibility: hidden; }
#tphelp.open #tphelp-drawer { visibility: visible; }
#tphelp.open #tphelp-drawer { transform: translateX(0); }

 
.tph-head { background: linear-gradient(135deg,#2563eb,#1d4ed8); color: #fff; padding: 15px 16px 14px; flex-shrink: 0; }
.tph-head-row { display: flex; align-items: center; gap: 10px; }
.tph-head-ico { width: 34px; height: 34px; border-radius: 10px; background: rgba(255,255,255,.16); display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.tph-head h3 { margin: 0; font-size: 16px; font-weight: 700; line-height: 1.15; }
.tph-head p { margin: 1px 0 0; font-size: 12px; opacity: .82; }
.tph-head .tph-htxt { flex: 1; min-width: 0; }
.tph-head .tph-htxt h3, .tph-head .tph-htxt p { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tph-iconbtn { background: rgba(255,255,255,.15); border: none; color: #fff; width: 32px; height: 32px; border-radius: 9px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: background .15s; font-size: 14px; }
.tph-iconbtn:hover { background: rgba(255,255,255,.3); }
.tph-back { display: none; }
#tphelp.has-back .tph-back { display: flex; }

 
.tph-search { position: relative; margin-top: 13px; }
.tph-search input { width: 100%; border: none; border-radius: 11px; padding: 11px 38px 11px 38px; font: inherit; font-size: 14px; background: rgba(255,255,255,.97); color: #0f172a; box-shadow: 0 2px 8px rgba(15,23,42,.12); }
.tph-search input:focus { outline: none; box-shadow: 0 0 0 3px rgba(255,255,255,.5); }
.tph-search .tph-sico { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: #94a3b8; font-size: 14px; pointer-events: none; }
.tph-search .tph-clear { position: absolute; right: 9px; top: 50%; transform: translateY(-50%); border: none; background: #e2e8f0; color: #64748b; width: 22px; height: 22px; border-radius: 50%; cursor: pointer; display: none; align-items: center; justify-content: center; font-size: 11px; }
.tph-search .tph-clear:hover { background: #cbd5e1; }
.tph-search.has-val .tph-clear { display: flex; }

 
.tph-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 16px; -webkit-overflow-scrolling: touch; }
.tph-body::-webkit-scrollbar { width: 9px; }
.tph-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9px; border: 2px solid #f6f8fb; }

 
.tph-foot { flex-shrink: 0; border-top: 1px solid #e7ecf3; background: #fff; padding: 11px 14px; display: flex; align-items: center; gap: 10px; }
.tph-foot-txt { flex: 1; min-width: 0; font-size: 12.5px; color: #64748b; }
.tph-foot-txt strong { color: #1e293b; font-weight: 600; display: block; font-size: 13px; }
.tph-foot button { flex-shrink: 0; display: inline-flex; align-items: center; gap: 7px; border: none; background: #2563eb; color: #fff; font: inherit; font-size: 13px; font-weight: 600; padding: 9px 13px; border-radius: 9px; cursor: pointer; transition: background .15s; white-space: nowrap; }
.tph-foot button:hover { background: #1d4ed8; }

 
.tph-sec-title { font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #94a3b8; margin: 4px 2px 9px; }
.tph-sec-title:not(:first-child) { margin-top: 20px; }

.tph-ctx { background: linear-gradient(135deg,#eff6ff,#f5f3ff); border: 1px solid #dbe6fb; border-radius: 14px; padding: 12px 13px; margin-bottom: 4px; }
.tph-ctx-h { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 700; color: #1d4ed8; margin-bottom: 9px; }
.tph-ctx-h i { font-size: 12px; }
.tph-ctx a { display: flex; align-items: center; gap: 9px; padding: 8px 9px; border-radius: 9px; text-decoration: none; color: #1e293b; font-size: 13.5px; font-weight: 500; transition: background .12s; }
.tph-ctx a:hover { background: rgba(255,255,255,.75); }
.tph-ctx a i.lead { color: #2563eb; font-size: 13px; width: 16px; text-align: center; flex-shrink: 0; }
.tph-ctx a .go { margin-left: auto; color: #94a3b8; font-size: 11px; }

.tph-quick { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.tph-quick a { display: flex; align-items: center; gap: 9px; background: #fff; border: 1px solid #e7ecf3; border-radius: 11px; padding: 11px 11px; text-decoration: none; color: #1e293b; font-size: 13px; font-weight: 600; transition: border-color .12s, box-shadow .12s, transform .12s; min-width: 0; }
.tph-quick a:hover { border-color: #bfd3f3; box-shadow: 0 6px 16px -8px rgba(37,99,235,.3); transform: translateY(-1px); }
.tph-quick a span.qtxt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tph-quick a .qi { width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; }

.tph-cats { display: flex; flex-direction: column; gap: 8px; }
.tph-cat { display: flex; align-items: center; gap: 12px; background: #fff; border: 1px solid #e7ecf3; border-radius: 13px; padding: 13px 14px; cursor: pointer; transition: border-color .12s, box-shadow .12s, transform .12s; text-align: left; width: 100%; font: inherit; }
.tph-cat:hover { border-color: #cdd7e5; box-shadow: 0 8px 20px -10px rgba(15,23,42,.2); transform: translateY(-1px); }
.tph-cat .ci { width: 42px; height: 42px; border-radius: 11px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
.tph-cat .ct { flex: 1; min-width: 0; }
.tph-cat .ct h4 { margin: 0; font-size: 14.5px; font-weight: 600; color: #0f172a; }
.tph-cat .ct p { margin: 2px 0 0; font-size: 12.5px; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tph-cat .cc { margin-left: auto; flex-shrink: 0; color: #cbd5e1; font-size: 13px; }

 
.tph-cathead { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.tph-cathead .ci { width: 46px; height: 46px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.tph-cathead h2 { margin: 0; font-size: 18px; font-weight: 700; }
.tph-cathead p { margin: 2px 0 0; font-size: 13px; color: #64748b; }
.tph-group { margin-bottom: 18px; }
.tph-group-t { font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #94a3b8; margin: 0 2px 8px; }
.tph-arts { display: flex; flex-direction: column; gap: 7px; }
.tph-art { display: flex; align-items: center; gap: 11px; background: #fff; border: 1px solid #e7ecf3; border-radius: 11px; padding: 12px 13px; cursor: pointer; transition: border-color .12s, box-shadow .12s; text-align: left; width: 100%; font: inherit; }
.tph-art:hover { border-color: #bfd3f3; box-shadow: 0 5px 15px -8px rgba(37,99,235,.28); }
.tph-art i.lead { color: #2563eb; font-size: 14px; width: 18px; text-align: center; flex-shrink: 0; }
.tph-art span { flex: 1; min-width: 0; font-size: 13.8px; font-weight: 500; color: #1e293b; }
.tph-art i.go { color: #cbd5e1; font-size: 12px; }

 
.tph-article h1 { font-size: 19px; font-weight: 700; margin: 0 0 4px; line-height: 1.25; }
.tph-crumb { font-size: 12px; color: #94a3b8; font-weight: 600; margin-bottom: 14px; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; background: none; border: none; padding: 0; }
.tph-crumb:hover { color: #2563eb; }
.tph-prose { font-size: 13.8px; color: #334155; line-height: 1.62; }
.tph-prose p { margin: 0 0 11px; }
.tph-prose strong { color: #0f172a; font-weight: 650; }
.tph-prose code { background: #eef2f7; border: 1px solid #e2e8f0; border-radius: 5px; padding: 1px 5px; font-size: 12.3px; font-family: ui-monospace, "SF Mono", Menlo, monospace; color: #1e293b; word-break: break-word; }
.tph-prose h4.tph-sub { font-size: 13px; font-weight: 700; color: #0f172a; margin: 16px 0 7px; }
.tph-prose ul { margin: 0 0 12px; padding-left: 2px; list-style: none; }
.tph-prose ul li { position: relative; padding-left: 19px; margin-bottom: 6px; }
.tph-prose ul li::before { content: ""; position: absolute; left: 4px; top: 8px; width: 5px; height: 5px; border-radius: 50%; background: #93c5fd; }
.tph-steps { counter-reset: s; margin: 0 0 13px; padding: 0; list-style: none; }
.tph-steps li { counter-increment: s; position: relative; padding-left: 31px; margin-bottom: 9px; }
.tph-steps li::before { content: counter(s); position: absolute; left: 0; top: -1px; width: 21px; height: 21px; border-radius: 50%; background: #2563eb; color: #fff; font-size: 11.5px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.tph-cal { border-radius: 10px; padding: 10px 12px; font-size: 13px; line-height: 1.55; margin: 0 0 13px; display: flex; gap: 9px; }
.tph-cal i { margin-top: 2px; flex-shrink: 0; }
.tph-cal-tip { background: #eff6ff; border: 1px solid #cfe0fb; color: #1e40af; }
.tph-cal-tip i { color: #2563eb; }
.tph-cal-warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
.tph-cal-warn i { color: #d97706; }
.tph-cal-danger { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
.tph-cal-danger i { color: #dc2626; }

.tph-links { display: flex; flex-direction: column; gap: 8px; margin: 16px 0 4px; }
.tph-link { display: flex; align-items: center; gap: 10px; background: #fff; border: 1px solid #d6e2f7; border-radius: 11px; padding: 11px 13px; text-decoration: none; color: #1d4ed8; font-size: 13.5px; font-weight: 650; transition: background .12s, border-color .12s, box-shadow .12s; }
.tph-link:hover { background: #eff6ff; border-color: #bfd3f3; box-shadow: 0 6px 16px -9px rgba(37,99,235,.4); }
.tph-link > i:first-child { font-size: 14px; width: 18px; text-align: center; flex-shrink: 0; }
.tph-link span { flex: 1; min-width: 0; }
.tph-link .tph-link-go { color: #93b4ed; font-size: 12px; }

.tph-related { margin-top: 22px; border-top: 1px solid #e7ecf3; padding-top: 14px; }

 
.tph-results { display: flex; flex-direction: column; gap: 8px; }
.tph-result { display: flex; align-items: flex-start; gap: 11px; background: #fff; border: 1px solid #e7ecf3; border-radius: 11px; padding: 12px 13px; cursor: pointer; transition: border-color .12s, box-shadow .12s; text-align: left; width: 100%; font: inherit; }
.tph-result:hover { border-color: #bfd3f3; box-shadow: 0 5px 15px -8px rgba(37,99,235,.28); }
.tph-result .ri { width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; margin-top: 1px; }
.tph-result .rt { flex: 1; min-width: 0; }
.tph-result .rt h4 { margin: 0; font-size: 13.8px; font-weight: 600; color: #0f172a; }
.tph-result .rt p { margin: 2px 0 0; font-size: 12px; color: #94a3b8; line-height: 1.45; }
.tph-empty { text-align: center; padding: 46px 24px; color: #94a3b8; }
.tph-empty i { font-size: 32px; color: #cbd5e1; margin-bottom: 13px; }
.tph-empty p { margin: 0; font-size: 13.5px; }
.tph-empty .sub { font-size: 12.5px; margin-top: 4px; color: #b6c2d2; }

@media (max-width: 560px) {
    

    #tphelp-drawer { left: 0 !important; right: 0 !important; width: auto !important; max-width: 100% !important; }
    .tph-quick { grid-template-columns: 1fr; }
    .tph-body { padding: 15px 14px 22px; }
    .tph-head { padding-left: 14px; padding-right: 14px; }
    .tph-foot { padding-left: 12px; padding-right: 12px; }
    .tph-foot button span.lbl { display: none; }
    .tph-foot button { padding: 10px 12px; }
    #tphelp-bubble { right: 16px; bottom: 16px; }
}
</style>

<div id="tphelp">
    <button id="tphelp-bubble" type="button" aria-label="Open help center">
        <i class="fa-solid fa-circle-question"></i><span>Help</span>
    </button>
    <div id="tphelp-backdrop"></div>
    <aside id="tphelp-drawer" role="dialog" aria-label="Help Center" aria-modal="true">
        <div class="tph-head">
            <div class="tph-head-row">
                <button class="tph-iconbtn tph-back" id="tphelp-back" type="button" aria-label="Back"><i class="fa-solid fa-arrow-left"></i></button>
                <div class="tph-head-ico" id="tphelp-head-ico"><i class="fa-solid fa-life-ring"></i></div>
                <div class="tph-htxt">
                    <h3 id="tphelp-title">Help Center</h3>
                    <p id="tphelp-sub">Search guides &amp; find any setting</p>
                </div>
                <button class="tph-iconbtn" id="tphelp-close" type="button" aria-label="Close"><i class="fa-solid fa-xmark"></i></button>
            </div>
            <div class="tph-search" id="tphelp-searchwrap">
                <i class="fa-solid fa-magnifying-glass tph-sico"></i>
                <input type="text" id="tphelp-input" placeholder="Search help… (e.g. transcoding, logo, email)" autocomplete="off" spellcheck="false">
                <button class="tph-clear" id="tphelp-clear" type="button" aria-label="Clear"><i class="fa-solid fa-xmark"></i></button>
            </div>
        </div>
        <div class="tph-body" id="tphelp-body"></div>
        <div class="tph-foot">
            <div class="tph-foot-txt"><strong>Still stuck?</strong>Reach a human on the support desk.</div>
            <button id="tphelp-contact" type="button"><i class="fa-solid fa-headset"></i> <span class="lbl">Contact support</span></button>
        </div>
    </aside>
</div>

<script>
(function () {
    if (window.__tpHelpInit) return; window.__tpHelpInit = true;

    var HELP_CTX = <?= json_encode($tphCtx, JSON_UNESCAPED_SLASHES) ?>;

    var TINTS = {
        sky:     { fg: '#2563eb', bg: '#eff6ff' },
        violet:  { fg: '#7c3aed', bg: '#f5f3ff' },
        emerald: { fg: '#059669', bg: '#ecfdf5' },
        fuchsia: { fg: '#c026d3', bg: '#fdf4ff' },
        indigo:  { fg: '#4f46e5', bg: '#eef2ff' },
        teal:    { fg: '#0d9488', bg: '#f0fdfa' },
        amber:   { fg: '#d97706', bg: '#fffbeb' },
        rose:    { fg: '#e11d48', bg: '#fff1f2' },
        cyan:    { fg: '#0891b2', bg: '#ecfeff' },
        slate:   { fg: '#475569', bg: '#f1f5f9' }
    };

    
    var HELP = [
        {
            id: 'getting-started', title: 'Getting started', icon: 'fa-rocket', tint: 'sky',
            blurb: 'Find your way around & get online',
            groups: [{ title: null, items: [
                { id: 'gs-welcome', title: 'Welcome to your CMS', kw: 'start overview intro begin first dashboard what is this admin home',
                  body: '<p>This is your <strong>admin dashboard</strong> — the control room for your tube site. Everything here runs on <strong>your own server and database</strong>; nothing is shared with anyone else.</p><p>There are two sides to your site:</p><ul><li>The <strong>public site</strong> your visitors browse.</li><li>This <strong>admin</strong>, where you add content and configure everything.</li></ul><p>Use <strong>View Site</strong> at the bottom of the sidebar to preview the public side in a new tab at any moment.</p><h4 class="tph-sub">Three things to know</h4><ul><li>The <strong>left sidebar</strong> is your main navigation (it collapses behind a ☰ button on mobile).</li><li>The thin <strong>status bar</strong> at the very top shows live server health and any running jobs.</li><li>Settings <strong>save instantly</strong> when you click Save — no separate publish step.</li></ul>',
                  links: [{ l: 'Open the dashboard', u: '/admin', i: 'fa-gauge-high' }, { l: 'View your public site', u: '/', i: 'fa-up-right-from-square', x: true }] },
                { id: 'gs-tour', title: 'A quick tour of the admin', kw: 'sidebar menu navigation layout sections where header status bar map',
                  body: '<p>The sidebar groups everything you manage:</p><ul><li><strong>Dashboard</strong> — your overview and recent activity.</li><li><strong>Videos</strong> — your library, with Categories, Tags, Pornstars and Channels nested under it.</li><li><strong>Users, Comments, Inbox</strong> — your members, comment moderation, and incoming messages/reports.</li><li><strong>Pages</strong> — static pages such as About or legal notices.</li><li><strong>Import</strong> — add videos from the catalogue, a file, or a feed.</li><li><strong>Plugins, Themes, Languages</strong> — extend, restyle and translate the site.</li><li><strong>Settings</strong> — storage, transcoding, security, email and maintenance.</li></ul><p>When a feature is busy (importing, encoding, deleting), a live indicator appears in the top status bar — click it to jump straight there.</p>',
                  links: [{ l: 'Open the dashboard', u: '/admin', i: 'fa-gauge-high' }] },
                { id: 'gs-checklist', title: 'Go-live checklist', kw: 'launch setup first steps publish ready new site todo order sequence',
                  body: '<p>Setting up a fresh site? Work through these in order — each links straight to the right screen.</p><ol class="tph-steps"><li><strong>Brand it</strong> — upload your logo &amp; favicon and pick your colours.</li><li><strong>Connect the catalogue</strong> so you can import content and receive updates.</li><li><strong>Set up storage &amp; transcoding</strong> so uploaded videos get processed.</li><li><strong>Import videos</strong> and let them download/encode.</li><li><strong>Configure email</strong> so sign-ups and password resets actually send.</li><li><strong>Secure the admin</strong> — turn on 2FA and a CAPTCHA, and consider a custom login URL.</li><li><strong>SEO</strong> — set your title patterns and generate the sitemap.</li><li><strong>Legal pages</strong> — add 2257, DMCA, Privacy and Terms.</li><li><strong>Languages</strong> — enable the ones your audience needs.</li></ol><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-lightbulb"></i><div>Keep the site hidden from Google while you build (Settings → General → discourage search engines), and switch it off the day you launch.</div></div>',
                  links: [{ l: 'Add your logo', u: '/admin/appearance?sub=branding', i: 'fa-image' }, { l: 'Connect &amp; import', u: '/admin/import', i: 'fa-download' }, { l: 'Set up email', u: '/admin/settings?tab=advanced&sub=email', i: 'fa-envelope' }] },
                { id: 'gs-connect', title: 'Connect to the TubePress catalogue', kw: 'catalogue connect api key secret marketplace credits balance license account link',
                  body: '<p>Linking your site to <strong>TubePress</strong> is what unlocks the content and the marketplace:</p><ul><li>Browse and <strong>import videos</strong> from the catalogue.</li><li><strong>AI translations</strong> for your categories and tags.</li><li>Install <strong>paid themes &amp; plugins</strong> and receive <strong>updates</strong> for core, themes and plugins.</li></ul><h4 class="tph-sub">How to connect</h4><ol class="tph-steps"><li>Open the <strong>Import</strong> page.</li><li>Click <strong>Connect</strong> — a popup links your account in one click.</li><li>Prefer manual? Paste your <strong>API key</strong> and <strong>secret</strong> instead.</li></ol><p>A <strong>credit balance</strong> is tied to the connection — it pays for catalogue imports, AI translations and paid add-ons. You can check it from the Import page.</p>',
                  links: [{ l: 'Open Import &amp; connect', u: '/admin/import', i: 'fa-plug' }] }
            ] }]
        },
        {
            id: 'videos', title: 'Adding &amp; importing videos', icon: 'fa-film', tint: 'violet',
            blurb: 'Fill your site with content',
            groups: [
                { title: 'Import from the catalogue', items: [
                    { id: 'imp-catalogue', title: 'Import videos from the catalogue', kw: 'import catalogue add videos browse sources select download artists performers include thumbnails confirm filter quality ai translate translation language price per language cost credits',
                      body: '<p>The fastest way to fill your site. You must <strong>connect to the catalogue</strong> first, then everything happens on the <strong>Import</strong> page.</p><ol class="tph-steps"><li>Pick the <strong>video type</strong> (e.g. straight or hentai) and a <strong>source</strong>.</li><li>Use the filters (search, quality, duration, popularity) to narrow the grid, then <strong>tick</strong> the videos you want. Already-imported items are marked, so you never grab duplicates.</li><li>Click <strong>Import selected</strong> to open the confirmation dialog.</li><li>Choose what to bring across — keep the source <strong>view counts</strong>, which <strong>transcoding formats</strong> to produce, and (optionally) <strong>AI rewriting/translation</strong> of titles and descriptions. Rewrites are a flat price per video; translations are billed <strong>per target language</strong> — the dialog shows the per-language price and the total for your enabled languages (up to 30). Translations run after download in small batches of a few languages per request, so large language sets import reliably.</li><li>Confirm. The videos are queued into the <strong>download queue</strong>.</li></ol><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Imports come as either a <strong>download</strong> (the file is fetched to your storage and can transcode) or an <strong>embed</strong> (it plays from the source — transcoding options don’t apply). Catalogue credit is spent when you confirm.</div></div><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-database"></i><div>If all your storage is full the import is refused before any credit is spent — add storage first.</div></div>',
                      links: [{ l: 'Open Import', u: '/admin/import', i: 'fa-download' }, { l: 'Download queue', u: '/admin/import/downloads', i: 'fa-list-check' }] },
                    { id: 'imp-queue', title: 'The download queue', kw: 'download queue progress pause resume workers retry failed eta speed parallel monitor threads',
                      body: '<p>Every catalogue import lands here so you can watch it complete. The page refreshes itself, so just leave it open.</p><h4 class="tph-sub">What you can do</h4><ul><li><strong>Stat cards</strong> — counts for downloading, pending, completed and failed; click one to filter the list.</li><li><strong>Pause / Resume</strong> the whole queue at any time.</li><li><strong>Worker threads</strong> — how many downloads run at once (1–3). More = faster but heavier on bandwidth.</li><li><strong>Per item</strong> — live progress and ETA, with a Cancel button.</li><li><strong>Retry</strong> a failed item, or retry all failed at once.</li></ul><p>Downloads are <strong>crash-safe and resumable</strong> — a reboot or a brief network drop won’t lose progress; the item simply resumes. Once a file lands, if transcoding is on it’s queued for encoding automatically.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>Common failures: the source URL expired (404) or your storage filled up. Free space / check the source, then Retry.</div></div>',
                      links: [{ l: 'Open the download queue', u: '/admin/import/downloads', i: 'fa-list-check' }] }
                ] },
                { title: 'Other import methods', items: [
                    { id: 'imp-csv', title: 'Import from a CSV or JSON file', kw: 'csv json spreadsheet bulk file mapping columns upload import list',
                      body: '<p>Already have a list of videos in a spreadsheet or export? On the <strong>Import</strong> page, switch to the <strong>CSV / JSON</strong> tab.</p><ol class="tph-steps"><li>Upload your <code>.csv</code> or <code>.json</code> file.</li><li><strong>Map the columns</strong> to fields — title (required), description, embed/file URL, thumbnail, duration, categories, tags, performers, channels, a unique ID, published date and views.</li><li>Use the test/preview to check the first row maps correctly.</li><li>Run the import. Rows with a unique ID that already exists are skipped (or updated), so re-running a file is safe.</li></ol>',
                      links: [{ l: 'Open Import', u: '/admin/import', i: 'fa-file-csv' }] },
                    { id: 'imp-feed', title: 'Import from an RSS / MRSS feed', kw: 'feed rss mrss grabber auto subscribe xml schedule syndication grab',
                      body: '<p>Subscribe to a content feed and let it import on a schedule. On the <strong>Import</strong> page open the <strong>Feeds</strong> tab.</p><ol class="tph-steps"><li><strong>Add a source</strong> — give it a name and the feed URL, then preview to confirm it parses (MRSS, RSS, or CSV/JSON feeds are supported).</li><li>Choose <strong>embed</strong> or <strong>download</strong> mode, the video type, how often to fetch, how many pages to pull, and whether to publish straight away.</li><li>Save. It then fetches automatically; you can also <strong>Fetch now</strong> on demand.</li></ol><p>Items are de-duplicated by their unique ID, so the same video is never imported twice. Failed items can be re-queued.</p>',
                      links: [{ l: 'Open Import', u: '/admin/import', i: 'fa-rss' }] }
                ] },
                { title: 'Manage your videos', items: [
                    { id: 'vid-add', title: 'Add a single video manually', kw: 'add video manual embed upload new iframe file url single create',
                      body: '<p>Go to <strong>Videos → New</strong>. A video plays from <strong>one</strong> of two sources — not both:</p><ul><li><strong>Embed code</strong> — paste an iframe or URL from an external host. Nothing is stored on your server; it’s sanitised for safety.</li><li><strong>Upload a file</strong> — a video you host yourself. If transcoding is on, it’s processed into your formats.</li></ul><p>Then fill in the title, description, and link <strong>categories, tags, pornstars and a channel</strong>. Set the <strong>status</strong> (draft / published / private) on the right.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>If you upload a file <em>and</em> paste an embed, the uploaded file wins and the embed is cleared. Transcoding only runs on a <strong>published</strong> video with an uploaded file.</div></div>',
                      links: [{ l: 'Open Videos', u: '/admin/videos', i: 'fa-video' }] },
                    { id: 'vid-edit', title: 'Edit a video &amp; its translations', kw: 'edit metadata title description thumbnail translate language status change relations',
                      body: '<p>Click any video to edit it. You can change every field, replace the <strong>thumbnail</strong>, adjust the status, and add or remove its categories, tags, performers and channel.</p><h4 class="tph-sub">Translations</h4><p>If your site has more than one language, open the <strong>Translations</strong> (globe) control to add a per-language <strong>title and description</strong>. Leave a language blank to remove its translation. A localized URL slug is generated automatically.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Moving a video from Draft to Published the first time stamps its publish date. Re-publishing later keeps the original date.</div></div>',
                      links: [{ l: 'Open Videos', u: '/admin/videos', i: 'fa-video' }] },
                    { id: 'vid-status', title: 'Video statuses explained', kw: 'status published processing error draft private transcoding queued meaning hidden',
                      body: '<p>Each video carries a status that controls where it appears:</p><ul><li><strong>Published</strong> — live and listed everywhere.</li><li><strong>Draft</strong> — saved but hidden from the public site.</li><li><strong>Private</strong> — not listed or searchable; reachable only by people you give the link to.</li><li><strong>Processing</strong> — a temporary state while it transcodes; it returns to Published when done.</li><li><strong>Error</strong> — transcoding failed. Open the transcode queue to read the reason and retry.</li></ul><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Video missing from the site? It’s almost always set to Draft or Private — switch it to Published.</div></div>',
                      links: [{ l: 'Open Videos', u: '/admin/videos', i: 'fa-video' }, { l: 'Transcode queue', u: '/admin/settings?tab=transcode&sub=queue', i: 'fa-bars-progress' }] },
                    { id: 'vid-bulk', title: 'Bulk-delete videos', kw: 'bulk delete remove mass select all batch many clean purge',
                      body: '<p>On <strong>Videos</strong>, tick the rows you want — or apply a filter and use <strong>Select all matching</strong> to span every page of results — then click <strong>Delete</strong>. Large deletions run as a background job with a live progress bar you can cancel.</p><div class="tph-cal tph-cal-danger"><i class="fa-solid fa-triangle-exclamation"></i><div>This is permanent. The video file and everything attached to it — comments, likes, favourites, watch history and any queued transcode jobs — are removed, and the category/tag/performer counts are updated.</div></div>',
                      links: [{ l: 'Open Videos', u: '/admin/videos', i: 'fa-video' }] }
                ] }
            ]
        },
        {
            id: 'albums', title: 'Albums (image galleries)', icon: 'fa-images', tint: 'rose',
            blurb: 'Photo galleries alongside your videos',
            groups: [
                { title: null, items: [
                    { id: 'alb-what', title: 'What are Albums?', kw: 'albums galleries images photos pictures gallery content type pics what are',
                      body: '<p><strong>Albums</strong> are image galleries &mdash; a second content type that lives alongside your videos. An album is a set of photos with its own title, description, cover image, taxonomy and translations.</p><ul><li>Albums have their <strong>own pages</strong>: a listing at <code>/albums</code> and a gallery page at <code>/album/&lt;slug&gt;</code> with a lightbox (zoom, slideshow).</li><li>They use a <strong>separate taxonomy</strong> from videos &mdash; album Categories, Tags, Pornstars and Channels are managed independently.</li><li>Visitors can <strong>favourite, like/dislike, comment on and report</strong> a gallery, just like a video, and find their saved galleries under their profile.</li></ul><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-lightbulb"></i><div>Images can live on your own storage or be referenced directly from an external/CDN URL (handy for very large imported sets).</div></div>',
                      links: [{ l: 'Open Albums', u: '/admin/albums', i: 'fa-images' }, { l: 'View /albums', u: '/albums', i: 'fa-up-right-from-square', x: true }] },
                    { id: 'alb-manage', title: 'Add &amp; manage albums', kw: 'add create edit album gallery images cover upload reorder taxonomy translation status publish draft',
                      body: '<p>Open <strong>Albums</strong> in the sidebar. Create a gallery, add images (upload or paste image URLs), set a <strong>cover</strong>, write the title/description and link its <strong>categories, tags, pornstars and channel</strong>.</p><ol class="tph-steps"><li>Click <strong>New album</strong>, fill in the title and pick the <strong>type</strong> (e.g. straight / gay / hentai).</li><li>Add images; drag to reorder. The first image (or the one you choose) becomes the <strong>cover</strong>.</li><li>Set the <strong>status</strong> &mdash; Draft hides it, Published makes it live.</li><li>Add per-language <strong>translations</strong> via the globe control if your site is multilingual.</li></ol>',
                      links: [{ l: 'Open Albums', u: '/admin/albums', i: 'fa-images' }] },
                    { id: 'alb-import', title: 'Import albums (file &amp; feed)', kw: 'import albums csv json feed mrss rss upload file images media_type gallery bulk sample example',
                      body: '<p>Both import methods on the <strong>Import</strong> page create albums as well as videos.</p><h4 class="tph-sub">Upload File (CSV / JSON)</h4><p>A row becomes an album when it carries an <code>images</code> list (or a <code>media_type</code> column set to <em>album</em>) and no embed/video. Map an <strong>Images</strong> column (a delimited list of URLs, or a JSON array) and optionally a <strong>Type</strong> column.</p><h4 class="tph-sub">Feed Import (MRSS / RSS / CSV / JSON)</h4><p>A feed item becomes an album when it carries image media and no video &mdash; in MRSS that means one or more <code>&lt;media:content medium="image"&gt;</code> entries. Open the <strong>&ldquo;See a correct feed example&rdquo;</strong> panel on the Feeds tab for a copy-paste sample that mixes a video and a gallery.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Items are de-duplicated by their unique ID, so re-running a file or feed never creates duplicate galleries.</div></div>',
                      links: [{ l: 'Open Import', u: '/admin/import', i: 'fa-download' }] },
                    { id: 'alb-taxonomy', title: 'Album categories, tags, pornstars &amp; channels', kw: 'album taxonomy categories tags pornstars performers channels separate thumbnail cover organise',
                      body: '<p>Albums have their own taxonomy, separate from videos, under the <strong>Albums</strong> section of the sidebar. Manage them just like the video ones &mdash; rename, reorder, translate, set a manual image.</p><p>If you don&rsquo;t upload a manual image, each category/pornstar/channel automatically shows the <strong>cover of its best-performing gallery</strong> as its thumbnail.</p>',
                      links: [{ l: 'Album categories', u: '/admin/albums/categories', i: 'fa-folder' }, { l: 'Album pornstars', u: '/admin/albums/performers', i: 'fa-star' }, { l: 'Album channels', u: '/admin/albums/channels', i: 'fa-tv' }] },
                    { id: 'alb-appearance', title: 'Album cards &amp; gallery page look', kw: 'appearance album cards grid columns radius gallery page watch display view date title cover meta lightbox related download single gallery',
                      body: '<p>Two more <strong>Appearance</strong> screens fine-tune galleries. (The album <em>listing</em> layout &mdash; grid vs masonry, navigation and sidebar filters &mdash; lives on the <strong>Layout</strong> tab; see the previous article.)</p><h4 class="tph-sub">Cards</h4><p>The <strong>Cards</strong> screen&rsquo;s <strong>Albums</strong> sub-tab styles the gallery <strong>card</strong>: corner radius, grid gap, title lines and which meta (image count, date, views) shows. A live preview updates as you tweak.</p><h4 class="tph-sub">Watch (single gallery page)</h4><p>The <strong>Watch</strong> screen&rsquo;s <strong>Albums</strong> sub-tab controls the single gallery page at <code>/album/&lt;slug&gt;</code>: which blocks show (views, date, like/dislike, favourite, pornstars, channels, categories, tags), a lightbox <strong>download</strong> button, and the <strong>related galleries</strong> count.</p>',
                      links: [{ l: 'Album listing (Layout)', u: '/admin/appearance?sub=layout', i: 'fa-table-cells-large' }, { l: 'Album cards', u: '/admin/appearance?sub=cards', i: 'fa-table-cells' }, { l: 'Gallery page (Watch)', u: '/admin/appearance?sub=watch', i: 'fa-image' }] },
                    { id: 'alb-listing', title: 'Album listing: display, navigation &amp; sidebar', kw: 'albums listing grid masonry infinite scroll pagination pages sidebar filters categories channels pornstars tags show hide browse bar default display navigation appearance layout /albums',
                      body: '<p>The album <strong>listing</strong> page (<code>/albums</code> and every album archive) is configured under <strong>Appearance &rarr; Layout &rarr; Albums</strong>, alongside its per-page, per-row and default-sort settings.</p><h4 class="tph-sub">Default display</h4><p>Pick <strong>Grid</strong> (even rows) or <strong>Masonry</strong> (Pinterest-style columns that follow each cover&rsquo;s height). This is what visitors see first &mdash; they can still switch with the on-page toggle, and their choice is remembered on their device.</p><h4 class="tph-sub">Default navigation</h4><p>Pick <strong>Pages</strong> (numbered pagination) or <strong>Infinite</strong> scroll (the next page loads automatically as the visitor reaches the bottom).</p><h4 class="tph-sub">Sidebar filters</h4><p>Tick which taxonomy filters appear in the album <strong>sidebar</strong> and the &ldquo;All&hellip;&rdquo; browse bar: <strong>Categories</strong>, <strong>Channels</strong>, <strong>Pornstars</strong> and <strong>Tags</strong>. Untick one to remove it from the album pages.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-lightbulb"></i><div>A filter with no entries is hidden automatically &mdash; e.g. if nothing is tagged yet, the <strong>Tags</strong> section stays hidden even while it is ticked. Leave them all on and the sidebar always looks tidy.</div></div>',
                      links: [{ l: 'Appearance &rarr; Layout', u: '/admin/appearance?sub=layout', i: 'fa-table-cells-large' }, { l: 'View /albums', u: '/albums', i: 'fa-up-right-from-square', x: true }] },
                    { id: 'alb-ctr', title: 'Self-optimising gallery covers (CTR)', kw: 'ctr cover image best performing ranking thumbnail auto optimise plugin click through gallery trending',
                      body: '<p>With the <strong>CTR Ranking</strong> plugin active, albums get the same engagement-based ranking as videos &mdash; <em>plus</em> a per-image optimiser: the gallery image that earns the most clicks automatically becomes the <strong>cover</strong> and moves first, so every gallery keeps leading with its strongest shot. Pornstar, category and channel thumbnails follow the best-performing cover too.</p>',
                      links: [{ l: 'Open Plugins', u: '/admin/plugins', i: 'fa-plug' }] },
                    { id: 'alb-toggle', title: 'Turn the Albums feature on or off', kw: 'disable enable albums feature toggle hide remove off galleries settings seo 410',
                      body: '<p>Don&rsquo;t want galleries on your site? Open <strong>Settings &rarr; General</strong> and untick <strong>Albums (image galleries)</strong>.</p><p>When off, the whole feature is cleanly hidden: public album pages return <strong>HTTP 410</strong>, album links disappear from the menu, profiles, search and the <strong>sitemap</strong>, and album sections are hidden site-wide &mdash; so there are <strong>no SEO dead-ends</strong>. Your album data is kept and reappears the moment you switch it back on.</p>',
                      links: [{ l: 'Settings &rarr; General', u: '/admin/settings?tab=general', i: 'fa-toggle-on' }] },
                    { id: 'alb-purge', title: 'Reset or purge albums', kw: 'purge delete reset albums galleries maintenance bulk remove all data wipe',
                      body: '<p>To wipe galleries in bulk, use <strong>Settings &rarr; Maintenance &rarr; Data Purge</strong>, which includes <strong>Albums</strong> and the album taxonomy as separate, tick-to-confirm options.</p><div class="tph-cal tph-cal-danger"><i class="fa-solid fa-triangle-exclamation"></i><div>Purging is permanent &mdash; it removes the galleries and everything attached (comments, likes, favourites, reports) and frees their media. It does not touch your videos.</div></div>',
                      links: [{ l: 'Data Purge', u: '/admin/settings?tab=maintenance&sub=purge', i: 'fa-eraser' }] }
                ] }
            ]
        },
        {
            id: 'organising', title: 'Organising your catalogue', icon: 'fa-folder-tree', tint: 'emerald',
            blurb: 'Categories, tags, performers & channels',
            groups: [
                { title: 'Taxonomies', items: [
                    { id: 'org-categories', title: 'Categories', kw: 'category categories collections organise sort thumbnail group order',
                      body: '<p>Categories group videos into browsable collections. On the <strong>Categories</strong> page you can:</p><ul><li>Create and edit them with a name, description and <strong>thumbnail</strong> (auto-picked from the top video, or upload your own).</li><li>Set a <strong>sort order</strong> to control how they appear (categories are the only taxonomy with manual ordering).</li><li>Filter by “with videos / empty” and sort by name, video count or newest.</li></ul><p>Each category belongs to one video type, so the same name can exist separately for, say, straight and hentai.</p>',
                      links: [{ l: 'Open Categories', u: '/admin/categories', i: 'fa-folder' }] },
                    { id: 'org-tags', title: 'Tags', kw: 'tag tags keyword label discovery niche topic',
                      body: '<p>Tags are lightweight keywords (name only — no description) that help visitors discover related videos. Create and edit them on the <strong>Tags</strong> page, with search, “with videos / empty” filtering and sorting by name, count or newest.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Thin tag pages can dilute your SEO. You can mark tag pages <strong>noindex</strong> in SEO → Indexing if you have many sparse ones.</div></div>',
                      links: [{ l: 'Open Tags', u: '/admin/tags', i: 'fa-tag' }, { l: 'SEO indexing', u: '/admin/seo?tab=indexing', i: 'fa-list-check' }] },
                    { id: 'org-performers', title: 'Pornstars &amp; Artists (performers)', kw: 'performer pornstar artist model actor profile photo bio country birth date hentai label rename',
                      body: '<p>Performer profiles carry a <strong>photo, bio, country, birth date and an external link</strong>. Manage them on the <strong>Pornstars</strong> page, with search and filters by country, “has photo” and video count.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>On hentai content the same records are shown as <strong>“Artist”</strong> rather than “Pornstar”. You can change that wording for any language in <strong>Languages → Video Labels</strong>.</div></div>',
                      links: [{ l: 'Open Pornstars', u: '/admin/performers', i: 'fa-user' }, { l: 'Rename labels (Languages)', u: '/admin/languages', i: 'fa-language' }] },
                    { id: 'org-channels', title: 'Channels', kw: 'channel channels uploader publisher creator user account profile logo bio website badge imported',
                      body: '<p>A <strong>channel</strong> is who published a video — its uploader / creator. On the <strong>Channels</strong> page (nested under Videos) each channel has a <strong>name, description, website and logo</strong>; it shows as a badge on its videos and gets its own listing page (<code>/channel/&hellip;</code>). Channels are created automatically when you import (catalogue or CSV/JSON), or you can add them by hand.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div><strong>Channel = user:</strong> when <strong>Settings &rarr; General &rarr; &ldquo;Channels are users&rdquo;</strong> is on (the default), each imported channel also gets a <strong>user account</strong> that owns its videos, so a channel doubles as a member and appears in <strong>Users</strong>. Turn it off to keep channels as content-only profiles.</div></div>',
                      links: [{ l: 'Open Channels', u: '/admin/channels', i: 'fa-clapperboard' }, { l: 'Channels-are-users setting', u: '/admin/settings?tab=general', i: 'fa-toggle-on' }] }
                ] },
                { title: 'Power tools', items: [
                    { id: 'org-translate', title: 'AI bulk-translate categories &amp; tags', kw: 'translate ai bulk languages credits cost automatic translate all',
                      body: '<p>On the <strong>Categories</strong> or <strong>Tags</strong> page, the <strong>Translate All</strong> button auto-translates names into your enabled languages with AI.</p><ul><li>It spends a small amount of <strong>catalogue credit per item</strong>, so you need the catalogue connected with a balance.</li><li>Tick <strong>skip already-translated</strong> to avoid paying twice.</li><li>It runs as a job you can watch; <strong>cancelling refunds</strong> the items not yet done.</li></ul>',
                      links: [{ l: 'Open Categories', u: '/admin/categories', i: 'fa-folder' }, { l: 'Open Tags', u: '/admin/tags', i: 'fa-tag' }, { l: 'Connect catalogue', u: '/admin/import', i: 'fa-plug' }] },
                    { id: 'org-videotypes', title: 'Video types (e.g. straight vs hentai)', kw: 'video type hentai straight hetero gay niche url prefix slug label segment separate default',
                      body: '<p>Every video, category, tag, performer and channel belongs to a <strong>video type</strong>. This keeps niches cleanly separated and is reflected in your URLs and labels:</p><ul><li>The <strong>default</strong> type has no URL prefix (e.g. <code>/video/…</code>).</li><li>Other types get a prefix (e.g. <code>/hentai/video/…</code>).</li><li>Each type can have its own <strong>label</strong> (for example <strong>Artist</strong> instead of Pornstar) and its own URL slug.</li></ul><p>Manage the default type, each type’s <strong>display label</strong> and <strong>URL slug</strong> in <strong>Appearance → Video Types</strong>. Changing a slug keeps old links working via automatic redirects.</p>',
                      links: [{ l: 'Open Video Types', u: '/admin/appearance?sub=videotypes', i: 'fa-shapes' }] }
                ] }
            ]
        },
        {
            id: 'appearance', title: 'Appearance &amp; branding', icon: 'fa-paintbrush', tint: 'fuchsia',
            blurb: 'Logo, colours, layout & themes',
            groups: [
                { title: 'Branding', items: [
                    { id: 'app-logo', title: 'Upload your logo &amp; favicon', kw: 'logo favicon brand image header logo dark logo icon upload picture height',
                      body: '<p>Open <strong>Appearance → Branding</strong>. There you set:</p><ul><li><strong>Light logo</strong> — your main logo (PNG or SVG with a transparent background works best).</li><li><strong>Dark logo</strong> — used in dark mode; if left empty the light logo is reused.</li><li><strong>Favicon</strong> — the small icon in the browser tab.</li><li><strong>Logo height</strong> — separate sliders for desktop (16–80px, default 36) and mobile (12–56px, default 28).</li></ul><p>If no logo is uploaded, your site name is shown as text instead.</p>',
                      links: [{ l: 'Open Appearance → Branding', u: '/admin/appearance?sub=branding', i: 'fa-image' }] },
                    { id: 'app-colors', title: 'Brand colours &amp; dark mode', kw: 'colors colours theme palette primary dark mode background accent surface',
                      body: '<p><strong>Appearance → Colors</strong> gives you a full palette for both a <strong>light</strong> and a <strong>dark</strong> theme — primary colour plus background, surface, card, text, muted-text and border, each with an opacity control and a colour picker.</p><h4 class="tph-sub">Dark mode</h4><ul><li>Turn dark mode on or off for the whole site.</li><li>Choose whether visitors land in <strong>light</strong> or <strong>dark</strong> by default (they can toggle, and their choice is remembered).</li><li>Leave a dark colour blank and it sensibly inherits from the light one.</li></ul><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-lightbulb"></i><div>Keep enough contrast: dark text on light backgrounds, light text on dark — muted text should still be comfortably readable.</div></div>',
                      links: [{ l: 'Open Appearance → Colors', u: '/admin/appearance?sub=colors', i: 'fa-palette' }] }
                ] },
                { title: 'Layout & navigation', items: [
                    { id: 'app-menu', title: 'Edit the navigation menu', kw: 'menu navigation nav links reorder drag header items search',
                      body: '<p><strong>Appearance → Menu</strong> is a drag-and-drop editor for your top navigation. You can:</p><ul><li>Reorder the built-in items (Home, Categories, Tags, Pornstars, Channels, and the video-type switcher) and show/hide each.</li><li>Add <strong>custom links</strong> or link to your <strong>published pages</strong>.</li><li>Override each item’s <strong>label per language</strong> (e.g. rename “Pornstars” to “Models”).</li><li>Toggle the <strong>search box</strong> in the navbar.</li></ul>',
                      links: [{ l: 'Open Appearance → Menu', u: '/admin/appearance?sub=menu', i: 'fa-bars' }] },
                    { id: 'app-homepage', title: 'Homepage, header &amp; footer', kw: 'homepage header topbar announcement footer columns sections watch again sort',
                      body: '<p>Appearance has dedicated sub-tabs for the main page furniture:</p><ul><li><strong>Homepage</strong> — toggle the “watch again” strip and choose the default sort (Newest, Popular, Top Rated, or Trending if the CTR plugin is active).</li><li><strong>Header</strong> — an optional top announcement bar with its own text, link and colours.</li><li><strong>Footer</strong> — the footer columns and links.</li></ul>',
                      links: [{ l: 'Homepage', u: '/admin/appearance?sub=homepage', i: 'fa-house' }, { l: 'Header', u: '/admin/appearance?sub=header', i: 'fa-window-maximize' }, { l: 'Footer', u: '/admin/appearance?sub=footer', i: 'fa-shoe-prints' }] },
                    { id: 'app-layout', title: 'Layout, grid &amp; cards', kw: 'layout grid per page per row width padding cards thumbnail spacing density columns albums galleries masonry infinite scroll pagination sidebar filters navigation',
                      body: '<p><strong>Appearance → Layout</strong> controls the overall frame and grids:</p><ul><li><strong>Site max width</strong> (1000–2400px, default 1800) and page padding for desktop (0–80px) and mobile (0–40px).</li><li><strong>Videos</strong> per page (4–120) and per row (4–6).</li><li>Per-page / per-row counts and default sorts for <strong>categories, pornstars, channels and albums</strong> too — the <strong>Albums</strong> tab also sets the album listing&rsquo;s <strong>default display</strong> (Grid or Masonry), <strong>default navigation</strong> (Pages or Infinite scroll) and which <strong>sidebar filters</strong> (categories, channels, pornstars, tags) appear on <code>/albums</code>.</li></ul><p><strong>Appearance → Cards</strong> styles the thumbnail card, with separate <strong>Videos</strong> and <strong>Albums</strong> sub-tabs: corner radius, grid gap, hover preview, title lines and which meta to show. A live preview updates as you tweak.</p>',
                      links: [{ l: 'Layout', u: '/admin/appearance?sub=layout', i: 'fa-table-cells' }, { l: 'Cards', u: '/admin/appearance?sub=cards', i: 'fa-clone' }] },
                    { id: 'app-watch', title: 'Watch &amp; gallery pages', kw: 'watch player video page quality download loop related playback speed fullscreen show hide albums galleries',
                      body: '<p><strong>Appearance → Watch</strong> has separate <strong>Videos</strong> and <strong>Albums</strong> sub-tabs for what each content page shows.</p><h4 class="tph-sub">Videos</h4><p>Toggle views, duration, date, like/dislike, favourite and the pornstars / channels / categories / tags blocks; set the player <strong>default quality</strong>, playback speed, loop, download, double-click fullscreen and the <strong>related videos</strong> count (2–20).</p><h4 class="tph-sub">Albums</h4><p>The same show/hide toggles for the single gallery page, a lightbox <strong>download</strong> button, and the <strong>related galleries</strong> count (0–24, 0 hides).</p>',
                      links: [{ l: 'Open Appearance → Watch', u: '/admin/appearance?sub=watch', i: 'fa-circle-play' }] }
                ] },
                { title: 'Themes', items: [
                    { id: 'app-themes', title: 'Switch or install a theme', kw: 'theme themes install activate marketplace template design skin switch reset',
                      body: '<p>On the <strong>Themes</strong> page you can activate any installed theme, or install new ones from the marketplace (paid themes need a license / credits via your catalogue connection). You can also uninstall an inactive theme, or <strong>reset</strong> a theme’s saved customisations.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>“Reset data” permanently clears that theme’s colours, menu and other saved settings — there’s no undo.</div></div>',
                      links: [{ l: 'Open Themes', u: '/admin/themes', i: 'fa-brush' }] },
                    { id: 'app-theme-code', title: 'Edit theme code', kw: 'code editor html css js customize developer template advanced edit php',
                      body: '<p><strong>Themes → Edit Code</strong> opens an editor for the active theme’s template, CSS and JS files. It’s for developers — there’s no syntax checking, so a mistake can break the page.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>A theme update can overwrite edited files. Review the update’s diff before applying, keep a backup, and ideally track your changes in version control.</div></div>',
                      links: [{ l: 'Open the code editor', u: '/admin/themes/editor', i: 'fa-code' }] }
                ] }
            ]
        },
        {
            id: 'media', title: 'Storage &amp; transcoding', icon: 'fa-photo-film', tint: 'indigo',
            blurb: 'Where files live & how they’re encoded',
            groups: [
                { title: 'Storage', items: [
                    { id: 'st-storage', title: 'Storage servers', kw: 'storage disk ftp s3 webdav space full default backend drive capacity secure url',
                      body: '<p><strong>Settings → Storage</strong> is where your media is kept. You can run several backends side by side:</p><ul><li><strong>Local disk</strong>, <strong>FTP</strong>, <strong>S3</strong> or <strong>WebDAV</strong>.</li><li>Mark one as the <strong>Default</strong> — new uploads go there.</li><li>Enable/disable, edit, or delete a backend (you can only delete one that isn’t the default and holds no videos).</li><li>Optionally serve <strong>secure (signed) URLs</strong> so files can’t be hot-linked.</li></ul><p>Each backend shows its driver, video count, disk usage and a health indicator. New uploads switch over automatically when a disk passes its usage threshold.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-database"></i><div>If every storage is full, imports and uploads pause. Add space or another storage server to resume.</div></div>',
                      links: [{ l: 'Open Storage', u: '/admin/settings?tab=storage', i: 'fa-hard-drive' }] }
                ] },
                { title: 'Transcoding', items: [
                    { id: 'st-enable', title: 'How transcoding works &amp; turning it on', kw: 'transcode encode ffmpeg convert quality enable on processing what is',
                      body: '<p>Transcoding takes each <strong>uploaded</strong> video and re-encodes it into clean, web-ready MP4 versions at the resolutions you choose — and it’s also where your trim, watermark, intro/outro and hover-preview settings get applied. (Embedded videos aren’t transcoded — there’s no file to process.)</p><h4 class="tph-sub">Turning it on</h4><p>Flip the master switch at the top of <strong>Settings → Transcoding</strong>. It needs a way to actually encode:</p><ul><li><strong>FFmpeg installed on this server</strong> (<code>sudo apt install ffmpeg</code>), or</li><li>at least one <strong>remote transcoding server</strong> (see “Add a remote server”).</li></ul><p>If neither is available the switch is blocked with a notice explaining both options. Only <strong>published</strong> videos with an uploaded file are encoded.</p>',
                      links: [{ l: 'Open Transcoding', u: '/admin/settings?tab=transcode&sub=formats', i: 'fa-wand-magic-sparkles' }, { l: 'Add a server', u: '/admin/settings?tab=transcode&sub=servers', i: 'fa-server' }] },
                    { id: 'st-formats', title: 'Output formats (the quality ladder)', kw: 'formats resolution 720 1080 480 quality bitrate codec ladder trim renditions ffmpeg crf',
                      body: '<p><strong>Settings → Transcoding → Formats</strong> defines the versions produced from every source file. Each format has:</p><ul><li>A <strong>name</strong> and a <strong>file postfix</strong> (e.g. <code>_720p.mp4</code>).</li><li>A <strong>resolution</strong> (width × height) and a <strong>keep aspect ratio</strong> option.</li><li><strong>FFmpeg options</strong> — the raw encoding parameters. The default is a solid H.264 web profile: <code>-vcodec libx264 -movflags +faststart -crf 23 -preset medium -acodec aac -ar 44100 -ab 128k -f mp4</code>.</li><li>An <strong>active</strong> toggle, so you can keep a format defined but switched off.</li></ul><p>A <strong>Source</strong> format keeps the original resolution, and the encoder never upscales beyond the source. There’s also a <strong>Trim start</strong> (0–300s) that drops the first few seconds of every video.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-lightbulb"></i><div>A 1080p + 720p + 480p ladder suits most audiences. Lower the CRF for higher quality (bigger files), raise it for smaller files; <code>23</code> is a good middle ground.</div></div><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>Invalid FFmpeg options make that one rendition fail — check the Queue console if a format keeps erroring.</div></div>',
                      links: [{ l: 'Open Formats', u: '/admin/settings?tab=transcode&sub=formats', i: 'fa-sliders' }] },
                    { id: 'st-servers', title: 'Add a remote transcoding server', kw: 'server worker remote gpu cpu scale distribute offload node encode 8090 worker',
                      body: '<p>Encoding is CPU-heavy. <strong>Settings → Transcoding → Servers</strong> lets you process locally, on remote workers, or both — jobs <strong>auto-distribute to the least-busy server</strong>.</p><h4 class="tph-sub">This server (local)</h4><p>Set <strong>Concurrent jobs</strong>: 0 (remote-only, if a remote server is online), 1 (safe), 2 (VPS), 3 (dedicated) or 4 (powerful). You can also point it at a specific <strong>FFmpeg path</strong> if auto-detection doesn’t find it.</p><h4 class="tph-sub">Add a remote worker</h4><ol class="tph-steps"><li>Get a server with at least <strong>2 CPU cores and 2 GB RAM</strong> (a GPU is detected and used automatically).</li><li>Click <strong>Add Server</strong>, give it a name and URL (<code>http://YOUR-IP:8090</code>), and save — you’ll get a worker script and a start command with its API key.</li><li>Install FFmpeg on it: <code>sudo apt install ffmpeg php-cli php-curl</code>.</li><li>Upload the worker and open the port: <code>sudo ufw allow 8090/tcp</code> (and in your cloud firewall).</li><li>Start it: <code>php transcode-worker.php --port=8090 --key=…</code> (a systemd unit is provided to auto-start on boot).</li><li>Click <strong>Check</strong> — it should show <strong>Online</strong> with the detected encoder and live CPU/GPU.</li></ol><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-gauge-high"></i><div>Rule of thumb: <strong>1 job per 2 CPU cores</strong>. Add several servers to scale out, and put Nginx + HTTPS in front of a worker for production.</div></div>',
                      links: [{ l: 'Open Servers', u: '/admin/settings?tab=transcode&sub=servers', i: 'fa-server' }] },
                    { id: 'st-watermark', title: 'Watermark your videos', kw: 'watermark logo overlay brand protect stamp opacity position',
                      body: '<p><strong>Settings → Transcoding → Watermark</strong> burns a logo onto every encoded video.</p><ul><li>Upload a <strong>PNG</strong> (transparency recommended).</li><li><strong>Position</strong> — top/bottom × left/right, centre, or random (default bottom-right).</li><li><strong>Opacity</strong> 10–100% (default 80).</li><li><strong>Max width</strong> 5–50% of the video width (default 15).</li><li><strong>Padding</strong> 0–15% from the edges (default 10).</li></ul><p>A live preview shows the placement; the watermark is applied by FFmpeg to every rendition.</p>',
                      links: [{ l: 'Open Watermark', u: '/admin/settings?tab=transcode&sub=watermark', i: 'fa-stamp' }] },
                    { id: 'st-intros', title: 'Intro &amp; outro (pre / post-roll)', kw: 'intro outro preroll postroll bumper branding clip start end',
                      body: '<p><strong>Settings → Transcoding → Intro &amp; Outro</strong> attaches a short branded clip to the start (pre-roll) and/or end (post-roll) of every video. Enable each one and upload an <strong>MP4</strong> — any resolution works, it’s automatically resized to match each output format. You can replace or remove a clip at any time.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>The clip is re-encoded into every rendition, so keep it short (a few seconds) to avoid bloating processing time.</div></div>',
                      links: [{ l: 'Open Intro &amp; Outro', u: '/admin/settings?tab=transcode&sub=intros', i: 'fa-clapperboard' }] },
                    { id: 'st-timeline', title: 'Timeline hover previews', kw: 'timeline sprite scrub hover preview thumbnails seek storyboard screenshots',
                      body: '<p><strong>Settings → Transcoding → Timeline</strong> generates the strip of little screenshots that appear when a viewer hovers over the progress bar. Enable it, then set:</p><ul><li><strong>Number of screenshots</strong> — 5–60 (default 10; 10–20 is the sweet spot). More is smoother to scrub but makes a bigger sprite file.</li><li><strong>Start after</strong> — 0–300s (default 0), to skip intros or black frames at the start.</li></ul>',
                      links: [{ l: 'Open Timeline', u: '/admin/settings?tab=transcode&sub=timeline', i: 'fa-images' }] },
                    { id: 'st-queue', title: 'The transcode queue', kw: 'queue jobs progress retry failed encoding logs console monitor stuck',
                      body: '<p><strong>Settings → Transcoding → Queue</strong> is your live job monitor. Filter by All / Pending / Processing / Completed / Failed, open a <strong>per-job console</strong> to read its auto-updating log (stage, progress and any error), and <strong>retry failed</strong> jobs. This is the first place to look if a video is stuck in Processing or shows Error.</p>',
                      links: [{ l: 'Open Queue', u: '/admin/settings?tab=transcode&sub=queue', i: 'fa-bars-progress' }] }
                ] }
            ]
        },
        {
            id: 'community', title: 'Users &amp; community', icon: 'fa-users', tint: 'teal',
            blurb: 'Members, comments, reports',
            groups: [
                { title: 'Accounts', items: [
                    { id: 'usr-users', title: 'Manage users &amp; roles', kw: 'users accounts roles admin moderator member deactivate password ban create permissions',
                      body: '<p>The <strong>Users</strong> page lists every account. You can create, edit and deactivate them, and assign a role:</p><ul><li><strong>User</strong> — a regular member.</li><li><strong>Moderator</strong> — can moderate content, but cannot change roles or edit/delete admin and moderator accounts.</li><li><strong>Admin</strong> — full control, including granting roles.</li></ul><p>Deactivating keeps the account but blocks sign-in. For safety you can’t delete yourself, can’t remove the last admin, and can’t deactivate the last active admin.</p>',
                      links: [{ l: 'Open Users', u: '/admin/users', i: 'fa-user-gear' }] },
                    { id: 'usr-registration', title: 'Allow or block sign-ups', kw: 'registration signup register accounts enable disable join membership',
                      body: '<p>Public registration is toggled in <strong>Settings → General</strong> (the first Settings tab). Turn it off to make the site invite-only or fully admin-managed. Pair it with a <strong>CAPTCHA</strong> on the registration form to keep bots out.</p>',
                      links: [{ l: 'Open Settings → General', u: '/admin/settings?tab=general', i: 'fa-user-plus' }, { l: 'Set up CAPTCHA', u: '/admin/settings?tab=security&sub=captcha', i: 'fa-robot' }] }
                ] },
                { title: 'Moderation', items: [
                    { id: 'usr-comments', title: 'Comments &amp; moderation', kw: 'comments moderate approve spam pending enable review hold queue',
                      body: '<p>Enable comments — and optionally <strong>require approval</strong> before they appear — in <strong>Settings → General</strong>. Then moderate everything on the <strong>Comments</strong> page, filtering by All / Pending / Approved / Spam. For each comment you can <strong>approve</strong>, mark as <strong>spam</strong>, or <strong>delete</strong>, individually or in bulk.</p>',
                      links: [{ l: 'Open Comments', u: '/admin/comments', i: 'fa-comments' }, { l: 'Comment settings', u: '/admin/settings?tab=general', i: 'fa-gear' }] },
                    { id: 'usr-spam', title: 'Anti-spam for comments', kw: 'spam honeypot rate limit links duplicate bot block time gate length protection',
                      body: '<p><strong>Settings → General</strong> includes a stack of comment anti-spam guards you can switch on independently:</p><ul><li><strong>Honeypot</strong> — a hidden field only bots fill in.</li><li><strong>Time gate</strong> — a minimum number of seconds on the page before a comment is accepted.</li><li><strong>Rate limit</strong> — caps comments per IP per hour and per day.</li><li><strong>Duplicate block</strong> — rejects the same message posted again within a window.</li><li><strong>Link blocking</strong> — limits or forbids comments containing URLs.</li><li><strong>Length</strong> — enforces a minimum and maximum character count.</li></ul><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Honeypot + time gate + rate limit stop the vast majority of bot spam with no friction for real visitors.</div></div>',
                      links: [{ l: 'Open Settings → General', u: '/admin/settings?tab=general', i: 'fa-shield' }] },
                    { id: 'usr-inbox', title: 'The Inbox: messages &amp; reports', kw: 'inbox contact messages reports abuse dmca flag complaint email form',
                      body: '<p>The <strong>Inbox</strong> collects everything visitors send you, in two tabs:</p><ul><li><strong>Messages</strong> — submissions from your contact form. Read, reply or delete.</li><li><strong>Reports</strong> — content your visitors have flagged. Review and resolve or reject each one.</li></ul><p>The sidebar badge shows unread messages and pending reports so nothing slips through.</p>',
                      links: [{ l: 'Open the Inbox', u: '/admin/inbox', i: 'fa-inbox' }] }
                ] }
            ]
        },
        {
            id: 'email', title: 'Email &amp; notifications', icon: 'fa-envelope', tint: 'amber',
            blurb: 'Delivery, templates & alerts',
            groups: [{ title: null, items: [
                { id: 'em-transport', title: 'Set up email delivery', kw: 'email smtp resend mailgun brevo postmark sendgrid from deliver send transport provider php',
                  body: '<p>So password resets, contact replies and report notices actually arrive, set up delivery in <strong>Settings → Contact → Transport</strong>. Choose a <strong>method</strong> and fill its fields:</p><ul><li><strong>PHP mail</strong> — the server’s built-in mailer (simplest, least reliable for inboxing).</li><li><strong>SMTP</strong> — host, port, encryption, username, password.</li><li><strong>Resend / Mailgun / Brevo / Postmark / SendGrid</strong> — paste the provider’s API key (Mailgun also needs its domain/region).</li></ul><p>Set the <strong>From name and address</strong> and the <strong>contact recipient</strong> (where contact-form mail lands), then use <strong>Send test</strong> to confirm it works.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-check"></i><div>For reliable delivery, verify your sending domain with the provider and set <strong>From</strong> to an address on that domain — otherwise mail tends to land in spam.</div></div>',
                  links: [{ l: 'Open Transport', u: '/admin/settings?tab=advanced&sub=email', i: 'fa-paper-plane' }] },
                { id: 'em-templates', title: 'Customize email templates', kw: 'email types templates subject body password reset contact report preview placeholders edit content',
                  body: '<p><strong>Settings → Contact → Email types</strong> lists the transactional emails the site sends (such as password reset, contact forwarding and report notifications). For each one you can:</p><ul><li>Turn it <strong>on or off</strong>.</li><li>Override the <strong>subject</strong> and <strong>body</strong> (leave blank to use the built-in default).</li><li>Insert <strong>placeholders</strong> that get filled in with real data.</li><li>See a <strong>live preview</strong> in both desktop and mobile widths.</li></ul>',
                  links: [{ l: 'Open Email types', u: '/admin/settings?tab=advanced&sub=mails', i: 'fa-pen-to-square' }] },
                { id: 'em-telegram', title: 'Telegram notifications', kw: 'telegram bot notify alert chat push admin message instant',
                  body: '<p>Get pinged on Telegram when things happen. In <strong>Settings → Contact → Telegram</strong>:</p><ol class="tph-steps"><li>Create a bot with <strong>@BotFather</strong> and copy its <strong>token</strong>.</li><li>Get your <strong>chat ID</strong> (a personal chat is a positive number; a group/channel starts with <code>-100</code>).</li><li>Paste both, choose which <strong>events</strong> notify you (new contact message and new report are on by default; new user and new comment are off), and <strong>Send test</strong>.</li></ol>',
                  links: [{ l: 'Open Telegram', u: '/admin/settings?tab=advanced&sub=telegram', i: 'fa-paper-plane' }] }
            ] }]
        },
        {
            id: 'security', title: 'Security', icon: 'fa-shield-halved', tint: 'rose',
            blurb: 'Lock down your admin',
            groups: [{ title: null, items: [
                { id: 'sec-2fa', title: 'Two-factor authentication (2FA)', kw: '2fa two factor totp authenticator google authenticator otp login code secure qr',
                  body: '<p>Add a second lock to your admin login. In <strong>Settings → Security → 2FA</strong>:</p><ol class="tph-steps"><li>Click <strong>Enable</strong> — a QR code and secret appear.</li><li>Scan it with an authenticator app (Google Authenticator, Authy, 1Password…).</li><li>Enter the 6-digit code to confirm.</li></ol><p>From then on, logging in asks for a code from your app.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Turning 2FA off also requires a valid code, so nobody can disable it without your phone — keep your authenticator backed up.</div></div>',
                  links: [{ l: 'Open 2FA', u: '/admin/settings?tab=security&sub=2fa', i: 'fa-key' }] },
                { id: 'sec-captcha', title: 'CAPTCHA (reCAPTCHA / Cloudflare Turnstile)', kw: 'captcha recaptcha turnstile cloudflare bot spam register protect form google keys',
                  body: '<p>Stop bots on your public forms. In <strong>Settings → Security → Captcha</strong>, pick a provider:</p><ul><li><strong>Off</strong> — no challenge.</li><li><strong>Google reCAPTCHA</strong> — v2 (the checkbox) or v3 (invisible, score-based).</li><li><strong>Cloudflare Turnstile</strong> — privacy-friendly, usually no puzzle at all.</li></ul><p>Add the <strong>site key</strong> and <strong>secret key</strong> from the provider, choose a light/dark theme, and tick which forms to protect: <strong>registration, comments, contact</strong>.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>Both the site key and the secret key must be filled in, or the widget won’t appear. Each provider also needs your domain registered in its dashboard.</div></div>',
                  links: [{ l: 'Open Captcha', u: '/admin/settings?tab=security&sub=captcha', i: 'fa-robot' }] },
                { id: 'sec-admin-url', title: 'Change your admin login URL', kw: 'admin url login url slug hide obscure secret path move',
                  body: '<p>Move your login page off the obvious path so bots can’t hammer it. In <strong>Settings → Security → Admin URL</strong>, set a custom slug (3–50 characters, lowercase letters, numbers and hyphens). Your login becomes <code>yoursite.com/your-slug</code>; the dashboard itself stays at <code>/admin</code> once you’re in.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>This is obscurity, not real security — keep a strong password and 2FA on as well. If you ever forget the slug, you can clear it directly in the database.</div></div>',
                  links: [{ l: 'Open Admin URL', u: '/admin/settings?tab=security&sub=admin_url', i: 'fa-link' }] },
                { id: 'sec-hide', title: 'Hide the site from search (staging)', kw: 'noindex hide private staging search engines under construction discourage block google launch',
                  body: '<p>While you’re still building, turn on <strong>“Discourage search engines”</strong> in <strong>Settings → General</strong>. It adds a site-wide <em>noindex</em> and blocks crawlers in <code>robots.txt</code>, so nothing half-finished gets indexed.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>Switch this OFF on launch day — leaving it on keeps you invisible on Google.</div></div>',
                  links: [{ l: 'Open Settings → General', u: '/admin/settings?tab=general', i: 'fa-eye-slash' }, { l: 'SEO settings', u: '/admin/seo', i: 'fa-chart-line' }] }
            ] }]
        },
        {
            id: 'seo', title: 'SEO &amp; search engines', icon: 'fa-chart-line', tint: 'cyan',
            blurb: 'Get found on Google',
            groups: [
                { title: 'On-page SEO', items: [
                    { id: 'seo-titles', title: 'Page titles &amp; meta descriptions', kw: 'seo title meta description pattern template variables tags head snippet separator',
                      body: '<p><strong>SEO → Pages</strong> sets the title and meta-description <strong>pattern</strong> for each kind of page — home, video, category, tag, performer, channel, search and static pages.</p><p>Patterns use tokens that get filled in per page, such as <code>{title}</code>, <code>{site_name}</code>, <code>{count}</code>, <code>{pornstars}</code> and <code>{sep}</code> (your chosen separator). You can also add a “Page N” suffix to paginated titles.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-lightbulb"></i><div>Aim for titles under ~60 characters and descriptions under ~160 so Google doesn’t truncate them. Putting a keyword (and the performer name on video pages) near the front helps click-through.</div></div>',
                      links: [{ l: 'Open SEO → Pages', u: '/admin/seo?tab=pages&sub=home', i: 'fa-heading' }] },
                    { id: 'seo-social', title: 'Social cards (Open Graph)', kw: 'open graph og twitter card share facebook image preview social separator',
                      body: '<p><strong>SEO → Social</strong> controls how your links look when shared: the title separator, the <strong>Twitter/X card</strong> type and handle, your <strong>Facebook App ID</strong>, and a default <strong>Open Graph image</strong> (use ~1200×630px). If the CTR plugin is active, it can auto-generate share images from video thumbnails.</p>',
                      links: [{ l: 'Open SEO → Social', u: '/admin/seo?tab=social', i: 'fa-share-nodes' }] }
                ] },
                { title: 'Indexing', items: [
                    { id: 'seo-sitemap', title: 'Sitemap', kw: 'sitemap xml regenerate google index urls search console submit hreflang',
                      body: '<p><strong>SEO → Sitemap</strong> builds the XML sitemap search engines crawl. Choose what to include (videos, categories, tags, performers, channels, pages, image data, and hreflang for multi-language sites), set the max URLs per file (1,000–50,000; default 25,000), then click <strong>Regenerate</strong>. The page shows when it last ran.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Submit your sitemap URL once in Google Search Console — after that it keeps itself current. Regenerate after big content changes.</div></div>',
                      links: [{ l: 'Open SEO → Sitemap', u: '/admin/seo?tab=sitemap', i: 'fa-sitemap' }] },
                    { id: 'seo-robots', title: 'robots.txt', kw: 'robots crawl disallow bots crawler block allow',
                      body: '<p><strong>SEO → Robots</strong> gives you an editable <code>robots.txt</code>. The defaults already block admin, search, login and API paths, allow your theme assets so Google can render pages, slow down aggressive crawlers, and point to your sitemap. Only change it if you know the syntax.</p>',
                      links: [{ l: 'Open SEO → Robots', u: '/admin/seo?tab=robots', i: 'fa-file-lines' }] },
                    { id: 'seo-noindex', title: 'noindex thin pages', kw: 'noindex thin tags pagination search index control crawl budget categories',
                      body: '<p><strong>SEO → Indexing</strong> lets you keep low-value pages out of Google with a <em>noindex</em> tag. Search results and paginated pages are typically noindexed by default; you can also noindex tag, category, performer or channel listings if they’re thin. This focuses crawl budget on your strongest pages.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>Keep high-value landing pages (categories, performers) indexed; reach for noindex mainly on thin, auto-generated tag pages and search results.</div></div>',
                      links: [{ l: 'Open SEO → Indexing', u: '/admin/seo?tab=indexing', i: 'fa-list-check' }] }
                ] },
                { title: 'Tracking & verification', items: [
                    { id: 'seo-verify', title: 'Verify with search engines', kw: 'verification google bing yandex pinterest search console webmaster meta tag confirm ownership head',
                      body: '<p><strong>SEO → Advanced</strong> holds the verification codes for <strong>Google, Bing, Pinterest and Yandex</strong> — paste each to confirm ownership in their webmaster tools. There are also <strong>custom head and body code</strong> boxes for anything else you need to inject site-wide.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-triangle-exclamation"></i><div>Custom head/body code is inserted as-is and not sanitised — only paste code you trust.</div></div>',
                      links: [{ l: 'Open SEO → Advanced', u: '/admin/seo?tab=advanced', i: 'fa-circle-check' }] },
                    { id: 'seo-analytics', title: 'Analytics &amp; custom code', kw: 'analytics ga4 google analytics tracking head body pixel script tag manager',
                      body: '<p><strong>SEO → Analytics</strong> takes your <strong>GA4 measurement ID</strong> (format <code>G-XXXXXXXXXX</code>) and wires up Google Analytics for you. The same custom <strong>head</strong> and <strong>body</strong> code boxes are here too, for ad pixels, a tag manager or other scripts.</p>',
                      links: [{ l: 'Open SEO → Analytics', u: '/admin/seo?tab=analytics', i: 'fa-chart-simple' }] }
                ] }
            ]
        },
        {
            id: 'languages', title: 'Languages &amp; pages', icon: 'fa-language', tint: 'slate',
            blurb: 'Translate the UI & build static pages',
            groups: [
                { title: 'Languages', items: [
                    { id: 'lng-enable', title: 'Enable languages &amp; set the default', kw: 'language multilingual locale default enable add translate switch international auto detect',
                      body: '<p>On the <strong>Languages</strong> page, tick the languages you want to offer (up to eight), pick the <strong>default</strong>, and optionally let the site <strong>auto-detect</strong> the visitor’s browser language. English is always available and can’t be removed.</p>',
                      links: [{ l: 'Open Languages', u: '/admin/languages', i: 'fa-globe' }] },
                    { id: 'lng-translate', title: 'Translate the interface', kw: 'translate strings ui labels overrides wording rename artist pornstar text customize sections',
                      body: '<p>Pick a language on the <strong>Languages</strong> page to edit every interface string, grouped into clear sections (navigation, auth, video labels, comments, time, empty states and more). Each row shows the English source, the current value and an editable field.</p><p>Your edits are saved as <strong>overrides</strong> on top of the built-in translations, and you can <strong>reset any string</strong> back to default at any time.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-lightbulb"></i><div>This is also where you rebrand wording — for example change <strong>“Pornstar”</strong> to <strong>“Model”</strong> or <strong>“Artist”</strong>. Keep it consistent across the singular and plural labels.</div></div>',
                      links: [{ l: 'Open Languages', u: '/admin/languages', i: 'fa-language' }] },
                    { id: 'lng-slugs', title: 'Translate URL slugs', kw: 'slug url permalink localize path segment translate address',
                      body: '<p>In the <strong>URL Slugs</strong> section of the Languages editor you can localise the path segments (like <code>/videos</code> or <code>/categories</code>) per language, for clean native-looking URLs. A hint helps generate a tidy slug from the matching label; keep them lowercase and hyphenated.</p>',
                      links: [{ l: 'Open Languages', u: '/admin/languages', i: 'fa-link' }] }
                ] },
                { title: 'Static pages', items: [
                    { id: 'pg-create', title: 'Create a static page', kw: 'page about terms privacy dmca 2257 static content legal create footer contact form',
                      body: '<p><strong>Pages → New</strong> creates standalone pages such as About or Contact. Give each a <strong>title</strong>, an optional <strong>slug</strong> (auto-generated from the title), a rich <strong>body</strong>, and SEO meta. Then set its <strong>status</strong>, choose whether to <strong>show it in the footer</strong>, and optionally embed a <strong>contact form</strong>. The body is sanitised, so pasting from a document is safe. If you run multiple languages, you can translate each page.</p>',
                      links: [{ l: 'Open Pages', u: '/admin/pages', i: 'fa-file-lines' }] },
                    { id: 'pg-legal', title: 'Legal pages for an adult site', kw: 'legal 2257 dmca privacy terms compliance age removal content',
                      body: '<p>For an adult tube it’s strongly advisable to publish these as pages and link them in the footer:</p><ul><li><strong>18 U.S.C. 2257</strong> records / compliance statement</li><li><strong>DMCA</strong> &amp; copyright / content-removal policy</li><li><strong>Privacy Policy</strong></li><li><strong>Terms of Service</strong></li></ul><p>Add each in <strong>Pages</strong> and turn on “show in footer”. Put a <strong>contact form</strong> on your DMCA/removal page so takedown requests reach you.</p><div class="tph-cal tph-cal-warn"><i class="fa-solid fa-scale-balanced"></i><div>Requirements vary by country and these notes aren’t legal advice — tailor the wording to your jurisdiction, or have it reviewed.</div></div>',
                      links: [{ l: 'Open Pages', u: '/admin/pages', i: 'fa-scale-balanced' }] }
                ] }
            ]
        },
        {
            id: 'maintenance', title: 'Maintenance &amp; updates', icon: 'fa-screwdriver-wrench', tint: 'slate',
            blurb: 'Keep it healthy & current',
            groups: [
                { title: 'Updates & extensions', items: [
                    { id: 'mnt-updates', title: 'Update core, plugins &amp; themes', kw: 'update upgrade version core backup rollback changelog new release diff',
                      body: '<p>The <strong>Updates</strong> page shows anything with a newer version available (a badge also appears in the sidebar). Review the release notes — for themes you can preview the exact code diff first — then apply.</p><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-shield-halved"></i><div>A <strong>backup is taken automatically before every update</strong>, so if something looks wrong you can <strong>roll back</strong> to the previous version from the same page.</div></div>',
                      links: [{ l: 'Open Updates', u: '/admin/updates', i: 'fa-arrows-rotate' }] },
                    { id: 'mnt-autoupdate', title: 'Automatic updates', kw: 'auto automatic update unattended hands-off hands free scheduled overnight nightly daily signed verified rollback off default enable disable toggle keep current up to date core plugins themes manual paid auto_update_enabled',
                      body: '<p><strong>Settings → General</strong> has an <strong>Automatic updates (CMS &amp; plugins)</strong> switch. Turn it on and your site keeps itself current unattended — it checks once a day and applies any new release overnight, so you don’t have to visit the Updates page.</p><ul><li>Covers <strong>CMS core</strong> and <strong>free plugins</strong> only.</li><li>Every update is <strong>signature-verified</strong>, <strong>backed up first</strong>, and <strong>rolled back automatically if it fails</strong> — an auto-update can never leave your site broken.</li><li><strong>Themes always update manually</strong> (so you can preview the diff first), and <strong>paid plugins</strong> are left for you to apply.</li></ul><div class="tph-cal tph-cal-tip"><i class="fa-solid fa-circle-info"></i><div>It is <strong>off by default</strong>. Leave it off if you prefer to review each release yourself — you can always update by hand on the Updates page.</div></div>',
                      links: [{ l: 'Turn on automatic updates', u: '/admin/settings?tab=general', i: 'fa-toggle-on' }, { l: 'Open Updates', u: '/admin/updates', i: 'fa-arrows-rotate' }] },
                    { id: 'mnt-plugins', title: 'Install &amp; manage plugins', kw: 'plugin extension addon install activate deactivate marketplace feature module',
                      body: '<p><strong>Plugins</strong> add optional features. Install them from the marketplace (paid ones need a license / credits via your catalogue connection), then activate, deactivate or uninstall as needed. Active plugins can add their own settings and menu items.</p>',
                      links: [{ l: 'Open Plugins', u: '/admin/plugins', i: 'fa-puzzle-piece' }] }
                ] },
                { title: 'Health & data', items: [
                    { id: 'mnt-health', title: 'Server health checks', kw: 'health php extensions permissions diagnostics status check requirements',
                      body: '<p><strong>Settings → Maintenance → Server Health</strong> runs diagnostics across PHP, the database, the filesystem, disk space and memory, and flags anything that needs attention with a recommended fix. It’s read-only — a snapshot you can refresh — so apply fixes via your hosting panel or server.</p>',
                      links: [{ l: 'Open Server Health', u: '/admin/settings?tab=maintenance&sub=health', i: 'fa-heart-pulse' }] },
                    { id: 'mnt-purge', title: 'Data purge (danger zone)', kw: 'purge delete all reset wipe clear bulk remove data start over',
                      body: '<p><strong>Settings → Maintenance → Data Purge</strong> can permanently wipe whole datasets at once — all videos, all categories, all non-admin users, and so on — each shown with its current count. You must type <strong>DELETE</strong> to confirm, and admin accounts are always preserved.</p><div class="tph-cal tph-cal-danger"><i class="fa-solid fa-triangle-exclamation"></i><div>This cannot be undone. Use it only to reset a site or clear out test data before launch.</div></div>',
                      links: [{ l: 'Open Data Purge', u: '/admin/settings?tab=maintenance&sub=purge', i: 'fa-trash-can' }] },
                    { id: 'mnt-status', title: 'Reading the status bar', kw: 'status hdd ram load monitor disk cpu resources top bar health encoding import deleting',
                      body: '<p>The thin bar at the top of every admin page shows live server vitals: <strong>HDD</strong>, <strong>RAM</strong> and <strong>Load</strong>. Each is colour-coded — <span style="color:#16a34a;font-weight:600">green</span> is healthy, <span style="color:#ca8a04;font-weight:600">amber</span> is busy, <span style="color:#dc2626;font-weight:600">red</span> needs attention. When an <strong>Import</strong>, <strong>Encoding</strong> or <strong>Deleting</strong> job is running it appears there too with progress — click it to jump straight to that job.</p>',
                      links: [{ l: 'Open the dashboard', u: '/admin', i: 'fa-gauge-high' }] }
                ] }
            ]
        }
    ];

    
    var QUICK = [
        { id: 'app-logo',       label: 'Upload your logo', icon: 'fa-image',    tint: 'fuchsia' },
        { id: 'imp-catalogue',  label: 'Import videos',     icon: 'fa-download', tint: 'violet' },
        { id: 'st-enable',      label: 'Set up transcoding',icon: 'fa-wand-magic-sparkles', tint: 'indigo' },
        { id: 'em-transport',   label: 'Set up email',      icon: 'fa-envelope', tint: 'amber' },
        { id: 'sec-2fa',        label: 'Turn on 2FA',       icon: 'fa-key',      tint: 'rose' },
        { id: 'seo-sitemap',    label: 'Generate sitemap',  icon: 'fa-sitemap',  tint: 'cyan' }
    ];

    
    var ARTI = {}, CAT_OF = {}, ALL = [];
    HELP.forEach(function (cat) {
        cat.groups.forEach(function (g) {
            g.items.forEach(function (a) {
                ARTI[a.id] = a; CAT_OF[a.id] = cat; ALL.push(a);
            });
        });
    });

    
    function contextIds() {
        var m = HELP_CTX.menu || '', tab = HELP_CTX.tab || '', sub = HELP_CTX.sub || '';
        if (m === 'settings') {
            if (tab === 'security') return ({ '2fa': ['sec-2fa'], 'captcha': ['sec-captcha'], 'admin_url': ['sec-admin-url'] })[sub] || ['sec-2fa', 'sec-captcha', 'sec-admin-url'];
            if (tab === 'transcode') return ({ 'formats': ['st-formats', 'st-enable'], 'watermark': ['st-watermark'], 'intros': ['st-intros'], 'timeline': ['st-timeline'], 'servers': ['st-servers'], 'queue': ['st-queue'] })[sub] || ['st-enable', 'st-formats', 'st-servers'];
            if (tab === 'advanced') return ({ 'email': ['em-transport'], 'mails': ['em-templates'], 'telegram': ['em-telegram'] })[sub] || ['em-transport', 'em-templates', 'em-telegram'];
            if (tab === 'maintenance') return ({ 'purge': ['mnt-purge'], 'health': ['mnt-health'] })[sub] || ['mnt-health', 'mnt-purge'];
            if (tab === 'storage') return ['st-storage'];
            return ['usr-registration', 'usr-comments', 'usr-spam', 'sec-hide'];
        }
        var map = {
            dashboard:  ['gs-welcome', 'mnt-status'],
            videos:     ['vid-edit', 'vid-status', 'vid-bulk'],
            categories: ['org-categories', 'org-translate'],
            tags:       ['org-tags', 'org-translate'],
            performers: ['org-performers'],
            channels:    ['org-channels'],
            users:      ['usr-users', 'usr-registration'],
            comments:   ['usr-comments', 'usr-spam'],
            inbox:      ['usr-inbox'],
            pages:      ['pg-create', 'pg-legal'],
            import:     ['imp-catalogue', 'imp-queue', 'imp-csv', 'imp-feed'],
            plugins:    ['mnt-plugins'],
            themes:     ['app-themes', 'app-theme-code'],
            languages:  ['lng-translate', 'lng-enable', 'lng-slugs'],
            appearance: ['app-logo', 'app-colors', 'app-menu', 'app-layout', 'app-watch'],
            seo:        ['seo-titles', 'seo-sitemap', 'seo-robots'],
            updates:    ['mnt-updates', 'mnt-autoupdate']
        };
        return map[m] || [];
    }

    
    var root  = document.getElementById('tphelp');
    var bubble = document.getElementById('tphelp-bubble');
    var body  = document.getElementById('tphelp-body');
    var input = document.getElementById('tphelp-input');
    var searchWrap = document.getElementById('tphelp-searchwrap');
    var titleEl = document.getElementById('tphelp-title');
    var subEl   = document.getElementById('tphelp-sub');
    var backBtn = document.getElementById('tphelp-back');
    var headIco = document.getElementById('tphelp-head-ico');

    var stack = [];  

    function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
    function tint(t) { return TINTS[t] || TINTS.slate; }
    function plain(html) { var d = document.createElement('div'); d.innerHTML = html; return (d.textContent || '').replace(/\s+/g, ' ').trim(); }

    function setHeader(title, sub, showBack, icon) {
        titleEl.textContent = title;
        subEl.textContent = sub || '';
        if (showBack) root.classList.add('has-back'); else root.classList.remove('has-back');
        headIco.innerHTML = '<i class="fa-solid ' + (icon || 'fa-life-ring') + '"></i>';
    }

    function linkBtn(L) {
        var ext = L.x ? ' target="_blank" rel="noopener"' : '';
        var ic = L.i || 'fa-up-right-from-square';
        return '<a class="tph-link" href="' + L.u + '"' + ext + '><i class="fa-solid ' + ic + '"></i><span>' + L.l + '</span><i class="fa-solid fa-chevron-right tph-link-go"></i></a>';
    }

    
    function renderHome() {
        setHeader('Help Center', 'Search guides & find any setting', false, 'fa-life-ring');
        var html = '';

        var ctx = contextIds().filter(function (id) { return ARTI[id]; }).slice(0, 4);
        if (ctx.length) {
            html += '<div class="tph-ctx"><div class="tph-ctx-h"><i class="fa-solid fa-location-dot"></i> Suggested for this page</div>';
            ctx.forEach(function (id) {
                var a = ARTI[id];
                html += '<a href="#" data-art="' + id + '"><i class="fa-solid fa-circle-question lead"></i>' + a.title + '<i class="fa-solid fa-chevron-right go"></i></a>';
            });
            html += '</div>';
        }

        html += '<div class="tph-sec-title">Quick actions</div><div class="tph-quick">';
        QUICK.forEach(function (q) {
            var t = tint(q.tint);
            html += '<a href="#" data-art="' + q.id + '"><span class="qi" style="background:' + t.bg + ';color:' + t.fg + '"><i class="fa-solid ' + q.icon + '"></i></span><span class="qtxt">' + q.label + '</span></a>';
        });
        html += '</div>';

        html += '<div class="tph-sec-title">Browse all topics</div><div class="tph-cats">';
        HELP.forEach(function (cat) {
            var t = tint(cat.tint);
            html += '<button class="tph-cat" data-cat="' + cat.id + '">'
                + '<span class="ci" style="background:' + t.bg + ';color:' + t.fg + '"><i class="fa-solid ' + cat.icon + '"></i></span>'
                + '<span class="ct"><h4>' + cat.title + '</h4><p>' + cat.blurb + '</p></span>'
                + '<span class="cc"><i class="fa-solid fa-chevron-right"></i></span></button>';
        });
        html += '</div>';
        body.innerHTML = html;
        body.scrollTop = 0;
    }

    function renderCategory(catId) {
        var cat = null; HELP.forEach(function (c) { if (c.id === catId) cat = c; });
        if (!cat) { renderHome(); return; }
        var t = tint(cat.tint);
        setHeader(plain(cat.title), 'Help topic', true, cat.icon);
        var html = '<div class="tph-cathead"><span class="ci" style="background:' + t.bg + ';color:' + t.fg + '"><i class="fa-solid ' + cat.icon + '"></i></span><div><h2>' + cat.title + '</h2><p>' + cat.blurb + '</p></div></div>';
        cat.groups.forEach(function (g) {
            html += '<div class="tph-group">';
            if (g.title) html += '<div class="tph-group-t">' + g.title + '</div>';
            html += '<div class="tph-arts">';
            g.items.forEach(function (a) {
                html += '<button class="tph-art" data-art="' + a.id + '"><i class="fa-solid fa-circle-question lead"></i><span>' + a.title + '</span><i class="fa-solid fa-chevron-right go"></i></button>';
            });
            html += '</div></div>';
        });
        body.innerHTML = html;
        body.scrollTop = 0;
    }

    function renderArticle(artId) {
        var a = ARTI[artId]; if (!a) { renderHome(); return; }
        var cat = CAT_OF[artId]; var t = tint(cat.tint);
        setHeader(plain(a.title), plain(cat.title), true, cat.icon);
        var html = '<div class="tph-article">';
        html += '<button class="tph-crumb" data-cat="' + cat.id + '"><i class="fa-solid fa-folder-open"></i> ' + cat.title + '</button>';
        html += '<h1>' + a.title + '</h1>';
        html += '<div class="tph-prose">' + a.body + '</div>';
        if (a.links && a.links.length) {
            html += '<div class="tph-links">';
            a.links.forEach(function (L) { html += linkBtn(L); });
            html += '</div>';
        }
        var rel = [];
        cat.groups.forEach(function (g) { g.items.forEach(function (x) { if (x.id !== artId) rel.push(x); }); });
        if (rel.length) {
            html += '<div class="tph-related"><div class="tph-sec-title" style="margin-top:0">More in ' + cat.title + '</div><div class="tph-arts">';
            rel.slice(0, 4).forEach(function (x) {
                html += '<button class="tph-art" data-art="' + x.id + '"><i class="fa-solid fa-circle-question lead"></i><span>' + x.title + '</span><i class="fa-solid fa-chevron-right go"></i></button>';
            });
            html += '</div></div>';
        }
        html += '</div>';
        body.innerHTML = html;
        body.scrollTop = 0;
    }

    function renderSearch(q) {
        var needle = q.toLowerCase();
        var terms = needle.split(/\s+/).filter(Boolean);
        var scored = [];
        ALL.forEach(function (a) {
            var title = plain(a.title).toLowerCase();
            var hay = (title + ' ' + (a.kw || '') + ' ' + plain(a.body)).toLowerCase();
            var ok = terms.every(function (term) { return hay.indexOf(term) !== -1; });
            if (!ok) return;
            var score = 0;
            terms.forEach(function (term) {
                if (title.indexOf(term) !== -1) score += 10;
                if ((a.kw || '').toLowerCase().indexOf(term) !== -1) score += 4;
            });
            if (title.indexOf(needle) !== -1) score += 20;
            scored.push({ a: a, score: score });
        });
        scored.sort(function (x, y) { return y.score - x.score; });

        if (!scored.length) {
            body.innerHTML = '<div class="tph-empty"><i class="fa-solid fa-magnifying-glass"></i><p>No results for “' + esc(q) + '”</p><p class="sub">Try a simpler word — or contact support below.</p></div>';
            return;
        }
        var html = '<div class="tph-sec-title">' + scored.length + ' result' + (scored.length !== 1 ? 's' : '') + '</div><div class="tph-results">';
        scored.slice(0, 30).forEach(function (s) {
            var a = s.a, cat = CAT_OF[a.id], t = tint(cat.tint);
            var snippet = plain(a.body);
            if (snippet.length > 104) snippet = snippet.slice(0, 104) + '…';
            html += '<button class="tph-result" data-art="' + a.id + '"><span class="ri" style="background:' + t.bg + ';color:' + t.fg + '"><i class="fa-solid ' + cat.icon + '"></i></span><span class="rt"><h4>' + a.title + '</h4><p>' + esc(snippet) + '</p></span></button>';
        });
        html += '</div>';
        body.innerHTML = html;
        body.scrollTop = 0;
    }

    
    function go(view, arg, push) {
        if (push !== false) stack.push({ view: view, arg: arg });
        if (view === 'home') renderHome();
        else if (view === 'category') renderCategory(arg);
        else if (view === 'article') renderArticle(arg);
        backBtn.style.visibility = stack.length > 1 ? 'visible' : 'hidden';
    }
    function back() {
        if (input.value) { clearSearch(); return; }
        if (stack.length > 1) { stack.pop(); var cur = stack[stack.length - 1]; go(cur.view, cur.arg, false); }
        else go('home', null, false);
    }
    function resetTo(view, arg) { stack = [{ view: view, arg: arg }]; go(view, arg, false); }

    function clearSearch() {
        input.value = ''; searchWrap.classList.remove('has-val');
        var cur = stack[stack.length - 1] || { view: 'home' };
        if (cur.view === 'home') renderHome(); else if (cur.view === 'category') renderCategory(cur.arg); else if (cur.view === 'article') renderArticle(cur.arg); else renderHome();
    }

    
    function open() {
        root.classList.add('open');
        var sup = document.getElementById('tpsup'); if (sup) sup.classList.remove('open');  
        if (!stack.length) resetTo('home', null);
        setTimeout(function () { input.focus(); }, 320);
    }
    function close() { root.classList.remove('open'); }

    
    body.addEventListener('click', function (e) {
        var artEl = e.target.closest('[data-art]');
        if (artEl) { e.preventDefault(); go('article', artEl.getAttribute('data-art')); return; }
        var catEl = e.target.closest('[data-cat]');
        if (catEl) { e.preventDefault();
            var cid = catEl.getAttribute('data-cat');
            stack = [{ view: 'home' }, { view: 'category', arg: cid }];
            go('category', cid, false);
            backBtn.style.visibility = 'visible';
            return;
        }
    });

    bubble.addEventListener('click', open);
    document.getElementById('tphelp-close').addEventListener('click', close);
    document.getElementById('tphelp-backdrop').addEventListener('click', close);
    backBtn.addEventListener('click', back);
    document.getElementById('tphelp-clear').addEventListener('click', function () { clearSearch(); input.focus(); });
    document.getElementById('tphelp-contact').addEventListener('click', function () {
        close();
        var sb = document.getElementById('tpsup-bubble');
        if (sb) setTimeout(function () { sb.click(); }, 300);
    });

    var searchT;
    input.addEventListener('input', function () {
        var v = this.value.trim();
        if (v) searchWrap.classList.add('has-val'); else searchWrap.classList.remove('has-val');
        clearTimeout(searchT);
        searchT = setTimeout(function () {
            if (v) renderSearch(v); else clearSearch();
        }, 120);
    });

    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && root.classList.contains('open')) close();
    });
})();
</script>

<script>


(function () {
    var bubble = document.getElementById('tphelp-bubble');
    if (!bubble || !window.PointerEvent) return;
    var KEY = 'tphelp_pos_v1', MARGIN = 8, THRESH = 4;
    var dragging = false, moved = false, suppressClick = false;
    var startX = 0, startY = 0, baseLeft = 0, baseTop = 0;

    function clamp(left, top) {
        var w = bubble.offsetWidth, h = bubble.offsetHeight;
        return {
            left: Math.max(MARGIN, Math.min(left, window.innerWidth - w - MARGIN)),
            top:  Math.max(MARGIN, Math.min(top, window.innerHeight - h - MARGIN))
        };
    }
    function place(left, top, save) {
        var c = clamp(left, top);
        bubble.style.left = c.left + 'px';
        bubble.style.top = c.top + 'px';
        bubble.style.right = 'auto';
        bubble.style.bottom = 'auto';
        if (save) { try { localStorage.setItem(KEY, JSON.stringify(c)); } catch (e) {} }
    }
    function restore() {
        var raw; try { raw = localStorage.getItem(KEY); } catch (e) { return; }
        if (!raw) return;
        try { var p = JSON.parse(raw); if (p && isFinite(p.left) && isFinite(p.top)) place(p.left, p.top, false); } catch (e) {}
    }

    bubble.addEventListener('pointerdown', function (e) {
        if (e.button) return;                 
        dragging = true; moved = false;
        var r = bubble.getBoundingClientRect();
        baseLeft = r.left; baseTop = r.top;
        startX = e.clientX; startY = e.clientY;
    });
    bubble.addEventListener('pointermove', function (e) {
        if (!dragging) return;
        var dx = e.clientX - startX, dy = e.clientY - startY;
        if (!moved && (Math.abs(dx) > THRESH || Math.abs(dy) > THRESH)) {
            moved = true;
            bubble.classList.add('tphelp-dragging');
            try { bubble.setPointerCapture(e.pointerId); } catch (_) {}   
        }
        if (moved) { e.preventDefault(); place(baseLeft + dx, baseTop + dy, false); }
    });
    function end(e) {
        if (!dragging) return;
        dragging = false;
        try { bubble.releasePointerCapture(e.pointerId); } catch (_) {}
        if (moved) {
            bubble.classList.remove('tphelp-dragging');
            var r = bubble.getBoundingClientRect();
            place(r.left, r.top, true);
            suppressClick = true;                 
            setTimeout(function () { suppressClick = false; }, 60);
        }
        moved = false;
    }
    bubble.addEventListener('pointerup', end);
    bubble.addEventListener('pointercancel', end);

    
    document.addEventListener('click', function (e) {
        if (suppressClick && bubble.contains(e.target)) {
            e.stopImmediatePropagation(); e.preventDefault(); suppressClick = false;
        }
    }, true);

    window.addEventListener('resize', function () {
        if (bubble.style.left) place(parseFloat(bubble.style.left), parseFloat(bubble.style.top), true);
    });

    restore();
})();
</script>
