{"id":11,"date":"2026-03-02T05:11:19","date_gmt":"2026-03-02T05:11:19","guid":{"rendered":"https:\/\/portal.designedbyjohn.com\/?page_id=11"},"modified":"2026-03-02T05:13:36","modified_gmt":"2026-03-02T05:13:36","slug":"portal","status":"publish","type":"page","link":"https:\/\/portal.designedbyjohn.com\/","title":{"rendered":"Portal"},"content":{"rendered":"        <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\n        <script>\n            tailwind.config = {\n                theme: { extend: { colors: { primary: '#16a34a' } } }\n            }\n        <\/script>\n        <link rel=\"stylesheet\" href=\"https:\/\/cdn.jsdelivr.net\/npm\/flatpickr\/dist\/flatpickr.min.css\">\n        <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/flatpickr\"><\/script>\n        <style>\n        \/* Flatpickr theme override *\/\n        .flatpickr-calendar { background: var(--cp-card) !important; border-color: var(--cp-border) !important; box-shadow: 0 8px 24px rgba(0,0,0,0.4) !important; }\n        .flatpickr-day { color: var(--cp-text) !important; }\n        .flatpickr-day.selected, .flatpickr-day.selected:hover { background: var(--cp-primary) !important; border-color: var(--cp-primary) !important; }\n        .flatpickr-day:hover { background: var(--cp-bg) !important; }\n        .flatpickr-day.today { border-color: var(--cp-primary) !important; }\n        .flatpickr-months .flatpickr-month, .flatpickr-weekdays, .flatpickr-weekday { background: var(--cp-card) !important; color: var(--cp-muted) !important; fill: var(--cp-muted) !important; }\n        .flatpickr-current-month, .flatpickr-current-month .cur-month, .flatpickr-current-month input.cur-year { color: var(--cp-text) !important; }\n        .flatpickr-prev-month svg, .flatpickr-next-month svg { fill: var(--cp-muted) !important; }\n        .flatpickr-prev-month:hover svg, .flatpickr-next-month:hover svg { fill: var(--cp-primary) !important; }\n        .flatpickr-day.flatpickr-disabled, .flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay { color: var(--cp-border) !important; }\n        .flatpickr-input { cursor: pointer; }\n        .flatpickr-input.active { border-color: var(--cp-primary) !important; }\n        <\/style>\n        <style>\n            :root {\n                --cp-bg: #0a0a0a;\n                --cp-card: #171717;\n                --cp-text: #e5e5e5;\n                --cp-muted: #a3a3a3;\n                --cp-border: #262626;\n                --cp-primary: #16a34a;\n            }\n            body { background-color: var(--cp-bg); color: var(--cp-text); }\n            .cp-card { background-color: var(--cp-card); border: 1px solid var(--cp-border); }\n            html { margin-top: 0 !important; }\n            \/* Modal Styles *\/\n            .cp-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 50; align-items: center; justify-content: center; }\n            .cp-modal.active { display: flex; }\n            \/* Hover Utils *\/\n            .hover-primary:hover { color: var(--cp-primary); }\n            .border-hover-primary:hover { border-color: var(--cp-primary); }\n            header { background: var(--cp-bg) !important;}\n            \/* Checkerboard Background for Transparency *\/\n            .cp-checkerboard {\n                background-color: #fff;\n                background-image:\n                  linear-gradient(45deg, #ccc 25%, transparent 25%),\n                  linear-gradient(-45deg, #ccc 25%, transparent 25%),\n                  linear-gradient(45deg, transparent 75%, #ccc 75%),\n                  linear-gradient(-45deg, transparent 75%, #ccc 75%);\n                background-size: 20px 20px;\n                background-position: 0 0, 0 10px, 10px -10px, -10px 0px;\n            }\n            \/* Completion prompt active button *\/\n            .cp-btn-active { border-color: var(--cp-primary) !important; color: var(--cp-primary) !important; background: color-mix(in srgb, var(--cp-primary) 10%, transparent); }\n            \/* Admin status select \u2014 base style; JS applies color per status *\/\n            #adminStatusSelect { background: var(--cp-bg); color: var(--cp-text); border: 1px solid var(--cp-border); transition: border-color .2s, background .2s, color .2s; font-weight: 600; }\n            \/* Analytics dual-CTA buttons *\/\n            .cp-an-btn { border-color: var(--cp-border); background: var(--cp-card); color: var(--cp-muted); transition: border-color .15s, background .15s, color .15s; }\n            .cp-an-btn[data-selected=\"1\"][data-action=\"shared\"] { border-color: #15803d; background: rgba(21,128,61,0.15); color: #4ade80; }\n            .cp-an-btn[data-selected=\"1\"][data-action=\"create\"] { border-color: #1d4ed8; background: rgba(29,78,216,0.15); color: #60a5fa; }\n            \/* Lightbox *\/\n            #cp-lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.9); z-index: 100; align-items: center; justify-content: center; }\n            #cp-lightbox.active { display: flex; }\n            #cp-lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 8px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }\n        <\/style>\n\n        <div class=\"cp-wrapper min-h-screen font-sans antialiased relative\">\n                    <div class=\"flex flex-col justify-center items-center h-screen p-4\">\n            <div class=\"w-full max-w-md space-y-8 text-center\">\n                <h2 class=\"text-3xl font-bold tracking-tight mb-2\">Designed by John<\/h2>\n                <p class=\"text-sm text-[var(--cp-muted)]\">Client Portal<\/p>\n                <div class=\"cp-card py-8 px-6 shadow-xl rounded-xl text-left mt-8\">\n                    <!-- Login Form -->\n                    <div id=\"cpLoginPanel\">\n                        <form method=\"post\">\n                            <input type=\"hidden\" name=\"cp_login_action\" value=\"1\">\n                            <div class=\"space-y-4\">\n                                <div><label class=\"block text-sm font-medium text-[var(--cp-muted)] mb-1\">Client ID or Email<\/label><input type=\"text\" name=\"log\" required class=\"block w-full rounded-md border-[var(--cp-border)] bg-[var(--cp-bg)] text-[var(--cp-text)] px-4 py-3 focus:ring-1 focus:ring-[var(--cp-primary)] outline-none\"><\/div>\n                                <div><label class=\"block text-sm font-medium text-[var(--cp-muted)] mb-1\">Password<\/label><input type=\"password\" name=\"pwd\" required class=\"block w-full rounded-md border-[var(--cp-border)] bg-[var(--cp-bg)] text-[var(--cp-text)] px-4 py-3 focus:ring-1 focus:ring-[var(--cp-primary)] outline-none\"><\/div>\n                            <\/div>\n                                                        <button type=\"submit\" class=\"w-full flex justify-center rounded-md bg-[var(--cp-primary)] py-3 px-4 text-sm font-medium text-white hover:opacity-90 transition-all mt-6\">Access Dashboard<\/button>\n                        <\/form>\n                        <div class=\"mt-4 text-center\">\n                            <button type=\"button\" onclick=\"cpShowForgotPassword()\" class=\"text-xs text-[var(--cp-muted)] hover:text-[var(--cp-primary)] underline transition-colors\">Forgot your password?<\/button>\n                        <\/div>\n                    <\/div>\n                    <!-- Forgot Password Panel -->\n                    <div id=\"cpForgotPanel\" class=\"hidden\">\n                        <h3 class=\"font-semibold text-base mb-1\">Reset Password<\/h3>\n                        <p class=\"text-xs text-[var(--cp-muted)] mb-4\">Enter your email address and we'll send you a reset link.<\/p>\n                        <input type=\"email\" id=\"cpForgotEmail\" placeholder=\"Your email address\" class=\"block w-full rounded-md border-[var(--cp-border)] bg-[var(--cp-bg)] text-[var(--cp-text)] px-4 py-3 focus:ring-1 focus:ring-[var(--cp-primary)] outline-none mb-3\">\n                        <div id=\"cpForgotMsg\" class=\"hidden text-sm rounded px-3 py-2 mb-3\"><\/div>\n                        <button type=\"button\" onclick=\"cpSubmitForgotPassword()\" id=\"cpForgotBtn\" class=\"w-full flex justify-center rounded-md bg-[var(--cp-primary)] py-3 px-4 text-sm font-medium text-white hover:opacity-90 transition-all\">Send Reset Link<\/button>\n                        <div class=\"mt-4 text-center\">\n                            <button type=\"button\" onclick=\"cpShowLogin()\" class=\"text-xs text-[var(--cp-muted)] hover:text-[var(--cp-primary)] underline transition-colors\">Back to Login<\/button>\n                        <\/div>\n                    <\/div>\n                <\/div>\n                <script>\n                function cpShowForgotPassword() {\n                    document.getElementById('cpLoginPanel').classList.add('hidden');\n                    document.getElementById('cpForgotPanel').classList.remove('hidden');\n                }\n                function cpShowLogin() {\n                    document.getElementById('cpForgotPanel').classList.add('hidden');\n                    document.getElementById('cpLoginPanel').classList.remove('hidden');\n                }\n                function cpSubmitForgotPassword() {\n                    var email = document.getElementById('cpForgotEmail').value.trim();\n                    var msg   = document.getElementById('cpForgotMsg');\n                    var btn   = document.getElementById('cpForgotBtn');\n                    msg.className = 'hidden text-sm rounded px-3 py-2 mb-3';\n                    if (!email) {\n                        msg.textContent = 'Please enter your email address.';\n                        msg.className = 'text-sm rounded px-3 py-2 mb-3 bg-red-900\/20 text-red-400';\n                        return;\n                    }\n                    btn.disabled = true;\n                    btn.textContent = 'Sending\u2026';\n                    jQuery.post('https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', {\n                        action: 'cp_reset_password_email',\n                        nonce:  'b45f233d6d',\n                        email:  email\n                    }, function(res) {\n                        btn.disabled = false;\n                        btn.textContent = 'Send Reset Link';\n                        if (res.success) {\n                            msg.textContent = res.data || 'Reset link sent! Check your inbox.';\n                            msg.className = 'text-sm rounded px-3 py-2 mb-3 bg-green-900\/20 text-green-400';\n                            document.getElementById('cpForgotEmail').value = '';\n                        } else {\n                            msg.textContent = res.data || 'Could not send reset email.';\n                            msg.className = 'text-sm rounded px-3 py-2 mb-3 bg-red-900\/20 text-red-400';\n                        }\n                    }).fail(function() {\n                        btn.disabled = false;\n                        btn.textContent = 'Send Reset Link';\n                        msg.textContent = 'Network error. Please try again.';\n                        msg.className = 'text-sm rounded px-3 py-2 mb-3 bg-red-900\/20 text-red-400';\n                    });\n                }\n                <\/script>\n                <div class=\"mt-8 text-center\"><a href=\"?cp_demo=true\" class=\"text-xs text-[var(--cp-muted)] hover:text-[var(--cp-primary)] underline transition-colors\">View Intake Demo (No Login Required)<\/a><\/div>\n            <\/div>\n        <\/div>\n        <div id=\"modalContactPM\" class=\"cp-modal\"><div class=\"cp-card p-8 rounded-xl w-full max-w-sm relative text-center\"><button onclick=\"closeModal('modalContactPM')\" class=\"absolute top-4 right-4 text-[var(--cp-muted)] hover:text-[var(--cp-text)]\"><i data-lucide=\"x\" class=\"w-5 h-5\"><\/i><\/button><div class=\"w-12 h-12 bg-[var(--cp-bg)] rounded-full flex items-center justify-center mx-auto mb-4 text-[var(--cp-primary)]\"><i data-lucide=\"user\" class=\"w-6 h-6\"><\/i><\/div><h3 class=\"text-xl font-bold mb-2\">Contact Project Manager<\/h3><p class=\"text-sm text-[var(--cp-muted)] mb-6\">Need an account? Need help logging in? Contact your dedicated project manager.<\/p><div class=\"bg-[var(--cp-bg)] rounded-lg p-4 mb-4 border border-[var(--cp-border)]\"><div class=\"font-medium text-sm mb-1\">John<\/div><a href=\"mailto:john@designedbyjohn.com?subject=Account Request \/ Help - Client Portal&body=Hi John,%0D%0A%0D%0AI would like to request a new account.%0D%0A%0D%0AThanks,\" class=\"text-[var(--cp-primary)] text-sm font-bold hover:underline block break-all\">john@designedbyjohn.com<\/a><\/div><button onclick=\"copyToClipboard('john@designedbyjohn.com', this)\" class=\"w-full bg-[var(--cp-card)] border border-[var(--cp-border)] py-2 rounded text-sm hover:bg-[var(--cp-bg)] transition-colors flex items-center justify-center gap-2\"><i data-lucide=\"copy\" class=\"w-4 h-4\"><\/i> Copy Email<\/button><\/div><\/div>\n                <\/div>\n\n        <!-- Confirm Modal -->\n        <div id=\"cpConfirmModal\" class=\"hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black\/70 backdrop-blur-sm p-4\">\n            <div class=\"bg-[var(--cp-card)] border border-[var(--cp-border)] rounded-2xl shadow-2xl w-full max-w-sm\">\n                <div class=\"px-6 pt-6 pb-2 flex items-start gap-4\">\n                    <div class=\"shrink-0 w-10 h-10 rounded-full bg-amber-500\/10 flex items-center justify-center\">\n                        <i data-lucide=\"mail\" id=\"cpConfirmIcon\" class=\"w-5 h-5 text-amber-400\"><\/i>\n                    <\/div>\n                    <div>\n                        <h3 id=\"cpConfirmTitle\" class=\"font-semibold text-[var(--cp-text)] text-base mb-1\"><\/h3>\n                        <p id=\"cpConfirmMessage\" class=\"text-sm text-[var(--cp-muted)]\"><\/p>\n                    <\/div>\n                <\/div>\n                <div class=\"px-6 py-5 flex justify-end gap-3\">\n                    <button type=\"button\" id=\"cpConfirmCancel\" class=\"px-4 py-2 text-sm rounded-lg border border-[var(--cp-border)] text-[var(--cp-muted)] hover:text-[var(--cp-text)] transition-colors\">Cancel<\/button>\n                    <button type=\"button\" id=\"cpConfirmOk\" class=\"px-4 py-2 text-sm rounded-lg bg-[var(--cp-primary)] text-white hover:opacity-90 transition-opacity\">Send<\/button>\n                <\/div>\n            <\/div>\n        <\/div>\n\n        <!-- Image\/PDF Lightbox -->\n        <div id=\"cp-lightbox\" onclick=\"closeLightbox()\">\n            <div id=\"cp-lightbox-content\" onclick=\"event.stopPropagation()\"><\/div>\n            <button onclick=\"closeLightbox()\" class=\"absolute top-4 right-4 text-white hover:text-gray-300\"><i data-lucide=\"x\" class=\"w-8 h-8\"><\/i><\/button>\n        <\/div>\n\n        <script>\n          var cpNonce = 'b45f233d6d';\n\n          \/\/ Reusable styled confirmation modal\n          function cpConfirm(title, message, onConfirm, confirmLabel) {\n              var modal   = document.getElementById('cpConfirmModal');\n              document.getElementById('cpConfirmTitle').textContent   = title;\n              document.getElementById('cpConfirmMessage').textContent = message;\n              document.getElementById('cpConfirmOk').textContent      = confirmLabel || 'Send';\n              modal.classList.remove('hidden');\n              if (window.lucide) lucide.createIcons();\n\n              function cleanup() { modal.classList.add('hidden'); }\n\n              var okBtn     = document.getElementById('cpConfirmOk');\n              var cancelBtn = document.getElementById('cpConfirmCancel');\n\n              var newOk = okBtn.cloneNode(true);\n              okBtn.parentNode.replaceChild(newOk, okBtn);\n              var newCancel = cancelBtn.cloneNode(true);\n              cancelBtn.parentNode.replaceChild(newCancel, cancelBtn);\n\n              document.getElementById('cpConfirmOk').addEventListener('click', function() { cleanup(); onConfirm(); });\n              document.getElementById('cpConfirmCancel').addEventListener('click', cleanup);\n          }\n          var cpAssetMinWidth = 2000;\n          function openTutorialModal(url) {\n              let embedUrl = url;\n\n              \/\/ YouTube: youtube.com\/watch?v=ID  or  youtu.be\/ID\n              var ytMatch = url.match(\/(?:youtube\\.com\\\/watch\\?v=|youtu\\.be\\\/)([A-Za-z0-9_-]{11})\/);\n              if (ytMatch) {\n                  embedUrl = 'https:\/\/www.youtube.com\/embed\/' + ytMatch[1] + '?rel=0';\n              }\n              \/\/ Vimeo: vimeo.com\/ID  or  vimeo.com\/channels\/*\/ID  or  vimeo.com\/groups\/*\/video\/ID\n              else if (url.includes('vimeo.com') && !url.includes('player.vimeo.com')) {\n                  var vimeoMatch = url.match(\/vimeo\\.com\\\/(?:channels\\\/[^\\\/]+\\\/|groups\\\/[^\\\/]+\\\/video\\\/|video\\\/)?(\\d+)\/);\n                  if (vimeoMatch) {\n                      embedUrl = 'https:\/\/player.vimeo.com\/video\/' + vimeoMatch[1];\n                  }\n              }\n              \/\/ Already an embed URL (player.vimeo.com or youtube.com\/embed) \u2014 use as-is\n\n              document.getElementById('tutorialFrame').src = embedUrl;\n              document.getElementById('tutorialModal').classList.add('active');\n          }\n\n          function closeTutorialModal() {\n              document.getElementById('tutorialModal').classList.remove('active');\n              document.getElementById('tutorialFrame').src = ''; \/\/ Stop video playback\n          }\n            window.addEventListener('load', function() {\n                if(window.lucide) lucide.createIcons();\n                cpInitDatePickers();\n            });\n\n            \/\/ Initialize flatpickr on all date inputs \u2014 call after dynamic content is added too\n            function cpInitDatePickers(ctx) {\n                if (typeof flatpickr === 'undefined') return;\n                var root = ctx || document;\n                root.querySelectorAll('input[type=\"date\"]:not(.flatpickr-input)').forEach(function(el) {\n                    flatpickr(el, {\n                        dateFormat:  'Y-m-d',\n                        altInput:    true,\n                        altFormat:   'M j, Y',\n                        allowInput:  true,\n                        disableMobile: false,\n                    });\n                });\n            }\n\n            function openModal(id) { document.getElementById(id).classList.add('active'); }\n            function closeModal(id) { document.getElementById(id).classList.remove('active'); }\n\n            function openLightbox(url, type = 'image') {\n                const lb = document.getElementById('cp-lightbox');\n                const content = document.getElementById('cp-lightbox-content');\n                lb.classList.add('active');\n\n                if(type === 'pdf') {\n                    content.innerHTML = '<iframe src=\"'+url+'\" class=\"w-[90vw] h-[90vh] bg-white rounded-lg shadow-2xl\"><\/iframe>';\n                } else {\n                    content.innerHTML = '<img decoding=\"async\" src=\"'+url+'\" class=\"max-w-[90vw] max-h-[90vh] rounded-lg shadow-2xl cp-checkerboard\">';\n                }\n            }\n            function closeLightbox() {\n                document.getElementById('cp-lightbox').classList.remove('active');\n                document.getElementById('cp-lightbox-content').innerHTML = ''; \/\/ Clear content to stop video\/iframe\n            }\n\n            function copyToClipboard(text, btn) {\n                const originalHTML = btn.innerHTML;\n                const showSuccess = () => {\n                    btn.innerHTML = '<span class=\"text-green-500 flex items-center gap-2\"><i data-lucide=\"check\" class=\"w-4 h-4\"><\/i> Copied!<\/span>';\n                    if(window.lucide) lucide.createIcons();\n                    setTimeout(() => {\n                        btn.innerHTML = originalHTML;\n                        if(window.lucide) lucide.createIcons();\n                    }, 2000);\n                };\n\n                if (navigator.clipboard && navigator.clipboard.writeText) {\n                    navigator.clipboard.writeText(text).then(showSuccess).catch(() => fallbackCopy(text, showSuccess));\n                } else {\n                    fallbackCopy(text, showSuccess);\n                }\n            }\n\n            function fallbackCopy(text, successCallback) {\n                var textArea = document.createElement(\"textarea\");\n                textArea.value = text;\n                textArea.style.top = \"0\";\n                textArea.style.left = \"0\";\n                textArea.style.position = \"fixed\";\n                document.body.appendChild(textArea);\n                textArea.focus();\n                textArea.select();\n\n                try {\n                    var successful = document.execCommand('copy');\n                    if (successful) {\n                        successCallback();\n                    } else {\n                        alert('Please copy manually: ' + text);\n                    }\n                } catch (err) {\n                    alert('Please copy manually: ' + text);\n                }\n                document.body.removeChild(textArea);\n            }\n\n            \/\/ Voice to Text (Web Speech API)\n            var _cpVoiceRecog  = null;\n            var _cpVoiceBtn    = null;\n\n            function cpToggleVoice(btn, textareaId) {\n                var SR = window.SpeechRecognition || window.webkitSpeechRecognition;\n                if (!SR) {\n                    alert('Voice input is not supported in this browser. Please use Chrome or Edge.');\n                    return;\n                }\n\n                \/\/ If this button is already recording \u2014 stop\n                if (_cpVoiceBtn === btn && _cpVoiceRecog) {\n                    _cpVoiceRecog.stop();\n                    return;\n                }\n\n                \/\/ Stop any other active recording first\n                if (_cpVoiceRecog) { _cpVoiceRecog.stop(); }\n                if (_cpVoiceBtn)   { _cpVoiceSetInactive(_cpVoiceBtn); }\n\n                var textarea = document.getElementById(textareaId);\n                if (!textarea) return;\n\n                var baseText      = textarea.value;\n                var finalAppended = '';\n\n                _cpVoiceRecog = new SR();\n                _cpVoiceRecog.continuous      = true;\n                _cpVoiceRecog.interimResults  = true;\n                _cpVoiceRecog.lang            = 'en-US';\n\n                _cpVoiceRecog.onresult = function(event) {\n                    var interim = '', finals = '';\n                    for (var i = event.resultIndex; i < event.results.length; i++) {\n                        if (event.results[i].isFinal) {\n                            finals  += event.results[i][0].transcript;\n                        } else {\n                            interim += event.results[i][0].transcript;\n                        }\n                    }\n                    finalAppended += finals;\n                    var sep = baseText && !\/[\\s\\n]$\/.test(baseText + finalAppended) ? ' ' : '';\n                    textarea.value = baseText + (baseText ? ' ' : '') + finalAppended + interim;\n                };\n\n                _cpVoiceRecog.onerror = function(e) {\n                    if (e.error === 'not-allowed') {\n                        var msg = 'Microphone access was denied.\\n\\n';\n                        if (location.protocol !== 'https:') {\n                            msg += 'This page is served over HTTP. Voice input requires HTTPS. Please access the portal via a secure (https:\/\/) URL.';\n                        } else {\n                            msg += 'To enable voice input:\\n1. Click the lock icon in your browser address bar.\\n2. Set Microphone to \"Allow\".\\n3. Reload the page and try again.';\n                        }\n                        alert(msg);\n                    } else if (e.error !== 'no-speech' && e.error !== 'aborted') {\n                        alert('Microphone error: ' + e.error);\n                    }\n                    _cpVoiceSetInactive(btn);\n                    _cpVoiceRecog = null;\n                    _cpVoiceBtn   = null;\n                };\n\n                _cpVoiceRecog.onend = function() {\n                    \/\/ Commit clean final text\n                    var joined = (baseText + (baseText && finalAppended ? ' ' : '') + finalAppended).trim();\n                    textarea.value = joined;\n                    _cpVoiceSetInactive(btn);\n                    _cpVoiceRecog = null;\n                    _cpVoiceBtn   = null;\n                };\n\n                _cpVoiceRecog.start();\n                _cpVoiceBtn = btn;\n                _cpVoiceSetActive(btn);\n            }\n\n            function _cpVoiceSetActive(btn) {\n                btn.classList.add('text-red-500', 'border-red-700');\n                btn.classList.remove('text-green-500', 'border-[var(--cp-border)]');\n                btn.title = 'Stop recording';\n                btn.innerHTML = '<i data-lucide=\"mic-off\" class=\"w-4 h-4 animate-pulse\"><\/i> <span class=\"cp-voice-label text-xs\">Stop<\/span>';\n                if (window.lucide) lucide.createIcons();\n            }\n\n            function _cpVoiceSetInactive(btn) {\n                btn.classList.remove('text-red-500', 'border-red-700');\n                btn.classList.add('text-green-500', 'border-[var(--cp-border)]');\n                btn.title = 'Start recording';\n                btn.innerHTML = '<i data-lucide=\"mic\" class=\"w-4 h-4\"><\/i> <span class=\"cp-voice-label text-xs\">Dictate<\/span>';\n                if (window.lucide) lucide.createIcons();\n            }\n\n            \/\/ Password Toggle (Delegated Event for Robustness)\n            document.addEventListener('click', function(e) {\n                const btn = e.target.closest('.cp-toggle-pass');\n                if (btn) {\n                    e.preventDefault();\n                    const parent = btn.parentElement;\n                    const input = parent.querySelector('input');\n                    if (input) {\n                        const type = input.getAttribute('type') === 'password' ? 'text' : 'password';\n                        input.setAttribute('type', type);\n                    }\n                }\n            });\n\n            \/\/ Clear Credentials Action\n            function clearCredentials(pid) {\n                if(!confirm('SECURITY WARNING: This will permanently delete all login data (Usernames, Passwords, 2FA) from this project in the database. This cannot be undone. Are you sure?')) return;\n\n                jQuery.post('https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', {\n                    action: 'cp_clear_credentials',\n                    nonce: cpNonce,\n                    project_id: pid\n                }, function(res) {\n                    if(res.success) {\n                        alert('Credentials purged successfully.');\n                        location.reload();\n                    } else {\n                        alert('Error: ' + (res.data || 'Unknown error'));\n                    }\n                });\n            }\n\n            \/\/ File Management\n            function manageFile(action, pid, mod, id) {\n                let reason = '';\n                if(action === 'flag') {\n                    reason = prompt(\"Please provide a reason for flagging this file:\");\n                    if(!reason) return;\n                } else if(action === 'delete') {\n                    if(!confirm(\"Are you sure you want to delete this file?\")) return;\n                }\n\n                jQuery.post('https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', {\n                    action: 'cp_manage_file',\n                    nonce: cpNonce,\n                    do_action: action,\n                    project_id: pid,\n                    module: mod,\n                    file_id: id,\n                    reason: reason\n                }, function(res) {\n                    if(res.success) location.reload();\n                    else alert('Error: ' + (res.data || 'Failed'));\n                });\n            }\n\n            \/\/ Asset Validation \u2014 returns Promise<{file, valid, error?, info?}[]>\n            var CP_ALLOWED_IMAGE = ['image\/jpeg','image\/png','image\/webp','image\/avif','image\/tiff','image\/svg+xml'];\n            var CP_ALLOWED_VIDEO = ['video\/mp4','video\/quicktime','video\/webm'];\n\n            function validateAssetFiles(files) {\n                return Promise.all(Array.from(files).map(function(file) {\n                    return new Promise(function(resolve) {\n                        var isImage = CP_ALLOWED_IMAGE.indexOf(file.type) !== -1;\n                        var isVideo = CP_ALLOWED_VIDEO.indexOf(file.type) !== -1;\n\n                        if (!isImage && !isVideo) {\n                            resolve({ file: file, valid: false, error: file.name + ': Unsupported format. Allowed images: JPG, PNG, WebP, AVIF, TIFF, SVG. Allowed video: MP4, MOV, WebM.' });\n                            return;\n                        }\n\n                        if (isImage && file.type !== 'image\/svg+xml') {\n                            var img = new Image();\n                            var url = URL.createObjectURL(file);\n                            img.onload = function() {\n                                URL.revokeObjectURL(url);\n                                if (this.width < cpAssetMinWidth) {\n                                    resolve({ file: file, valid: false, error: file.name + ': Image is only ' + this.width + 'px wide \u2014 minimum ' + cpAssetMinWidth + 'px required. Please use a higher-resolution version.' });\n                                } else {\n                                    resolve({ file: file, valid: true, info: file.name + ' (' + this.width + ' \\u00d7 ' + this.height + 'px \\u2713)' });\n                                }\n                            };\n                            img.onerror = function() { URL.revokeObjectURL(url); resolve({ file: file, valid: false, error: file.name + ': Could not read image dimensions.' }); };\n                            img.src = url;\n                        } else if (isVideo) {\n                            var video = document.createElement('video');\n                            var vurl = URL.createObjectURL(file);\n                            video.onloadedmetadata = function() {\n                                URL.revokeObjectURL(vurl);\n                                if (this.videoHeight < cpAssetMinWidth) {\n                                    resolve({ file: file, valid: false, error: file.name + ': Video is only ' + this.videoHeight + 'px tall \u2014 minimum ' + cpAssetMinWidth + 'px height (1080p preferred) required.' });\n                                } else {\n                                    var label = this.videoHeight >= 1080 ? '1080p+' : this.videoHeight + 'px';\n                                    resolve({ file: file, valid: true, info: file.name + ' (' + label + ' \\u2713)' });\n                                }\n                            };\n                            video.onerror = function() { URL.revokeObjectURL(vurl); resolve({ file: file, valid: false, error: file.name + ': Could not read video dimensions.' }); };\n                            video.preload = 'metadata';\n                            video.src = vurl;\n                        } else {\n                            \/\/ SVG \u2014 no dimension restriction\n                            resolve({ file: file, valid: true, info: file.name + ' (SVG \\u2713)' });\n                        }\n                    });\n                }));\n            }\n\n            function toggleBrandFields(type) {\n                const chk = document.getElementById('no_'+type);\n                const container = document.getElementById('container_'+type);\n                const msg = document.getElementById('msg_'+type);\n                if(chk.checked) {\n                    container.classList.add('opacity-50', 'pointer-events-none');\n                    msg.classList.remove('hidden');\n                } else {\n                    container.classList.remove('opacity-50', 'pointer-events-none');\n                    msg.classList.add('hidden');\n                    \/\/ Clear the extra fields when unchecked so stale data isn't saved\n                    const dateInput = msg.querySelector('input[type=\"date\"]');\n                    const agencyChk = msg.querySelector('input[type=\"checkbox\"]');\n                    if (dateInput) dateInput.value = '';\n                    if (agencyChk) agencyChk.checked = false;\n                }\n            }\n\n            \/\/ Resend Credentials\n            function cpResendCredentials(userId, btn) {\n                cpConfirm(\n                    'Send Login Email?',\n                    'This will generate a new password and email the login credentials to the client.',\n                    function() {\n                        var origHTML = btn.innerHTML;\n                        btn.disabled = true;\n                        btn.innerHTML = '<i data-lucide=\"loader\" class=\"w-3 h-3 animate-spin\"><\/i> Sending...';\n                        jQuery.post('https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', {\n                            action: 'cp_resend_credentials',\n                            nonce: cpNonce,\n                            user_id: userId,\n                            portal_url: window.location.origin\n                        }, function(res) {\n                            btn.disabled = false;\n                            btn.innerHTML = origHTML;\n                            lucide.createIcons();\n                            if (res.success) { alert('Credentials sent successfully.'); }\n                            else { alert('Error: ' + (res.data || 'Failed to send.')); }\n                        });\n                    }\n                );\n            }\n\n            \/\/ Client Preview Toggle\n            function cpStartPreview(clientId, btn) {\n                var origHTML = btn.innerHTML;\n                btn.disabled = true;\n                btn.innerHTML = '<i data-lucide=\"loader\" class=\"w-3 h-3 animate-spin inline\"><\/i> Loading...';\n                jQuery.post('https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', {\n                    action: 'cp_start_client_preview',\n                    nonce: cpNonce,\n                    client_id: clientId\n                }, function(res) {\n                    if (res.success) {\n                        location.reload();\n                    } else {\n                        btn.disabled = false;\n                        btn.innerHTML = origHTML;\n                        alert('Error: ' + (res.data || 'Could not start preview.'));\n                    }\n                });\n            }\n\n            function cpStopPreview() {\n                jQuery.post('https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', {\n                    action: 'cp_stop_client_preview',\n                    nonce: cpNonce\n                }, function(res) {\n                    if (res.success) location.reload();\n                    else alert('Error: ' + (res.data || 'Could not exit preview.'));\n                });\n            }\n\n            \/\/ Admin Actions\n            function performAdminAction(action, id) {\n                if(!confirm('Are you sure you want to perform this action?')) return;\n\n                jQuery.post('https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', {\n                    action: 'cp_' + action,\n                    nonce: cpNonce,\n                    id: id\n                }, function(res) {\n                    if(res.success) { location.reload(); }\n                    else { alert('Error: ' + (res.data || 'Unknown error')); }\n                });\n            }\n\n            \/\/ Remove an existing team\/partner photo (clears hidden fields + hides the saved image)\n            function cpRemovePhoto(btn) {\n                var entry = btn.closest('.team-entry, .partner-entry');\n                if (!entry) return;\n                entry.querySelector('.cp-photo-id-field').value = '';\n                entry.querySelector('.cp-photo-url-field').value = '';\n                btn.closest('.cp-current-photo').remove();\n            }\n\n            \/\/ Photo input handler: preview + min-width validation + name population (team members)\n            document.addEventListener('change', function(e) {\n                if (!e.target || !e.target.classList.contains('cp-photo-input')) return;\n                var input = e.target;\n                var file = input.files && input.files[0];\n                if (!file) return;\n\n                var entry = input.closest('.team-entry, .partner-entry');\n                if (!entry) return;\n                var previewWrap = input.nextElementSibling;\n                var errorEl = previewWrap ? previewWrap.nextElementSibling : null;\n\n                if (errorEl) { errorEl.textContent = ''; errorEl.classList.add('hidden'); }\n                if (previewWrap) previewWrap.classList.add('hidden');\n\n                var doPreview = function() {\n                    if (!previewWrap) return;\n                    var img = previewWrap.querySelector('img');\n                    if (!img) return;\n                    var blobUrl = URL.createObjectURL(file);\n                    img.onload = function() { URL.revokeObjectURL(blobUrl); };\n                    img.src = blobUrl;\n                    previewWrap.classList.remove('hidden');\n                };\n\n                \/\/ Populate name from filename for team members (only if name field is empty)\n                if (entry.classList.contains('team-entry')) {\n                    var nameInput = entry.querySelector('input[name*=\"[name]\"]');\n                    if (nameInput && !nameInput.value.trim()) {\n                        var base = file.name.replace(\/\\.[^.]+$\/, '').replace(\/[-_]\/g, ' ');\n                        nameInput.value = base.replace(\/\\b\\w\/g, function(c) { return c.toUpperCase(); });\n                    }\n                }\n\n                \/\/ SVG \u2014 skip dimension check, just preview\n                if (file.type === 'image\/svg+xml') { doPreview(); return; }\n\n                var dimImg = new Image();\n                var blobUrl = URL.createObjectURL(file);\n                dimImg.onload = function() {\n                    URL.revokeObjectURL(blobUrl);\n                    if (this.width < cpAssetMinWidth) {\n                        if (errorEl) {\n                            errorEl.textContent = file.name + ': Image is only ' + this.width + 'px wide \u2014 minimum ' + cpAssetMinWidth + 'px required.';\n                            errorEl.classList.remove('hidden');\n                        }\n                        input.value = '';\n                    } else {\n                        doPreview();\n                    }\n                };\n                dimImg.onerror = function() { URL.revokeObjectURL(blobUrl); };\n                dimImg.src = blobUrl;\n            });\n\n            document.addEventListener('change', function(e) {\n                if (e.target && e.target.id === 'cp_asset_input') {\n                    var files = e.target.files;\n                    if (!files.length) return;\n\n                    var errorsEl = document.getElementById('assetValidationErrors');\n                    if (errorsEl) errorsEl.innerHTML = '<div class=\"text-[var(--cp-muted)] text-xs italic\">Checking files...<\/div>';\n\n                    validateAssetFiles(files).then(function(results) {\n                        if (errorsEl) errorsEl.innerHTML = '';\n\n                        var rejected = results.filter(function(r) { return !r.valid; });\n                        var accepted = results.filter(function(r) { return r.valid; });\n\n                        rejected.forEach(function(r) {\n                            if (errorsEl) errorsEl.innerHTML += '<div class=\"flex items-start gap-1.5 text-red-500 text-sm mt-1\"><i data-lucide=\"x-circle\" class=\"w-3.5 h-3.5 shrink-0 mt-0.5\"><\/i><span>' + r.error + '<\/span><\/div>';\n                        });\n\n                        if (accepted.length === 0) {\n                            if (errorsEl) errorsEl.innerHTML += '<div class=\"text-red-400 text-sm mt-2 font-semibold\">No valid files to upload. Please check requirements above.<\/div>';\n                            e.target.value = '';\n                            if (window.lucide) lucide.createIcons();\n                            return;\n                        }\n\n                        if (window.lucide) lucide.createIcons();\n\n                        var container = document.getElementById('upload-progress-container');\n                        var progressBar = document.getElementById('upload-progress-bar');\n                        var percentText = document.getElementById('upload-perc');\n                        var filenameText = document.getElementById('upload-filename');\n\n                        var formData = new FormData();\n                        formData.append('action', 'cp_save_module');\n                        formData.append('nonce', cpNonce);\n                        formData.append('project_id', '0');\n                        formData.append('module', 'assets');\n                        formData.append('cp_asset_tab', window.cpActiveAssetTab || 'General Use');\n                        accepted.forEach(function(r) { formData.append('files[]', r.file); });\n\n                        var xhr = new XMLHttpRequest();\n                        container.classList.remove('hidden');\n\n                        xhr.upload.addEventListener('progress', function(event) {\n                            if (event.lengthComputable) {\n                                var pct = Math.round((event.loaded \/ event.total) * 100);\n                                progressBar.style.width = pct + '%';\n                                percentText.innerHTML = pct + '%';\n                                filenameText.innerHTML = 'Uploading ' + accepted.length + ' of ' + results.length + ' file(s)...';\n                            }\n                        });\n\n                        xhr.onload = function() {\n                            try {\n                                var resp = JSON.parse(xhr.responseText);\n                                if (xhr.status === 200 && resp.success) {\n                                    if (resp.data && resp.data.drive_synced) {\n                                        progressBar.style.width = '100%';\n                                        percentText.innerHTML = '100%';\n                                        filenameText.innerHTML = '&#9729; Synced to Google Drive';\n                                        setTimeout(function() { if (window.cpClearDirty) cpClearDirty(); location.reload(); }, 1200);\n                                    } else {\n                                        if (window.cpClearDirty) cpClearDirty(); location.reload();\n                                    }\n                                } else {\n                                    container.classList.add('hidden');\n                                    alert('Upload failed: ' + (resp.data || 'Please try again.'));\n                                }\n                            } catch(ex) {\n                                container.classList.add('hidden');\n                                alert('Upload failed. Please try again.');\n                            }\n                        };\n\n                        xhr.open('POST', 'https:\/\/portal.designedbyjohn.com\/wp-admin\/admin-ajax.php', true);\n                        xhr.send(formData);\n                    });\n                }\n            });\n        <\/script>\n        \n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-11","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=\/wp\/v2\/pages\/11","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11"}],"version-history":[{"count":2,"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=\/wp\/v2\/pages\/11\/revisions"}],"predecessor-version":[{"id":15,"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=\/wp\/v2\/pages\/11\/revisions\/15"}],"wp:attachment":[{"href":"https:\/\/portal.designedbyjohn.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}