From 747d741293e064a979d7bf6c143ef969ea6d7629 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 24 May 2026 20:49:44 +0000
Subject: [PATCH] =GMBReview block minor tweaks. Refactored ReferralManager.php and ReferralRoutes.php to utilize the manager for all logic, and CustomTable for table interactions.

---
 assets/css/copy-hours.min.css                              |    2 
 build/gmbreviews/style-index.css                           |    2 
 build/gmbreviews/style-index-rtl.css                       |    2 
 build/drawer-menu/style-index-rtl.css                      |    2 
 base/email.php                                             |    5 
 build/drawer-menu/style-index.css                          |    2 
 src/gmbreviews/style.scss                                  |   14 
 build/feed/view.asset.php                                  |    2 
 jvb.php                                                    |    8 
 inc/rest/routes/ReferralRoutes.php                         |  201 ++---------
 src/drawer-menu/style.scss                                 |    6 
 /dev/null                                                  |    5 
 inc/managers/ReferralManager.php                           |  687 ++++++++++++++++++++++-----------------
 build/feed/view.js                                         |    2 
 src/gmbreviews/render.php                                  |    7 
 inc/blocks/CustomBlocks.php                                |    4 
 inc/registrar/Registrar.php                                |    4 
 build/gmbreviews/render.php                                |    7 
 inc/managers/SEO/render/Traits/_Properties/reviewTrait.php |   12 
 19 files changed, 474 insertions(+), 500 deletions(-)

diff --git a/assets/css/copy-hours.min.css b/assets/css/copy-hours.min.css
index d557a97..91348f2 100644
--- a/assets/css/copy-hours.min.css
+++ b/assets/css/copy-hours.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:0;bottom:0}.dashboard>header{justify-content:flex-end;position:fixed}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.crud form.table td .label,.crud form.table td label:not(.select-item-label):not(.radio-option){display:none}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:98vh;width:98vw;max-width:none;max-height:none;inset:0;margin:auto}dialog>.wrap{min-height:100%}dialog .item.upload.upload{display:flex;gap:1rem}dialog .item.upload .preview{width:40%}dialog .item.upload .group{width:60%}.upload details{width:100%}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}.dashboard #queue{bottom:0}
\ No newline at end of file
+.group-fields{position:relative}.hours-copy-btn:hover{background-color:rgb(var(--action-50));transform:scale(1.05)}.hours-copy-btn:active{transform:scale(.95)}.hours-copy-btn .icon{--w:0.875rem}.copy-hours-content h3{margin:0 0 1rem 0;color:rgb(var(--contrast));font-size:var(--txt-large)}.copy-hours-source{background-color:rgb(var(--base-100));padding:1rem;border-radius:var(--radius);margin-bottom:1.5rem;border:1px solid rgb(var(--base-200))}.copy-hours-source h4{margin:0 0 .5rem 0;color:rgb(var(--contrast-100));text-transform:uppercase;font-size:var(--txt-small);font-weight:600}.source-info{--gap:.25rem}.source-day{font-weight:600;color:rgb(var(--contrast));text-transform:capitalize}.source-hours{--gap:1rem;font-weight:500;color:rgb(var(--contrast))}.source-hours.closed{color:rgb(var(--contrast-200));font-style:italic}.copy-hours-targets{margin-bottom:2rem}.copy-hours-targets h4{margin:0 0 1rem 0;color:rgb(var(--contrast-100));text-transform:uppercase;font-size:var(--txt-small);font-weight:600}.day-checkboxes{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem}.feedback{position:fixed;top:2rem;right:2rem;background-color:rgb(var(--action-50));color:var(--action-contrast);padding:1rem 1.5rem;border-radius:var(--radius);box-shadow:rgba(var(--base),var(--op-45)) var(--shdw);z-index:10000;opacity:0;transform:translateX(100px);transition:all var(--trans-base);display:flex;align-items:center;gap:.5rem}.feedback.show{opacity:1;transform:translateX(0)}.feedback .icon{--w:1.25rem}
\ No newline at end of file
diff --git a/base/email.php b/base/email.php
index b6fcd4e..4a52d35 100644
--- a/base/email.php
+++ b/base/email.php
@@ -13,10 +13,7 @@
 	],
 	'types' => [
 		'newUser' => [
-			'subject'       => sprintf(
-				'Welcome to %s! Finish creating your account.',
-				get_bloginfo('name')
-			),
+			'subject'       => 'Welcome to '.get_bloginfo('name').'! Finish creating your account.',
 			'showPrefix'    => true,
 		],
 		'resetPass' => [
diff --git a/build/drawer-menu/style-index-rtl.css b/build/drawer-menu/style-index-rtl.css
index 38606b7..2ab52e9 100644
--- a/build/drawer-menu/style-index-rtl.css
+++ b/build/drawer-menu/style-index-rtl.css
@@ -1 +1 @@
-nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;left:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:rgb(var(--base));border-right:1px solid rgb(var(--base-200));box-shadow:rgba(var(--base),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(-180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid rgb(var(--contrast-200))}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}
+nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;left:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:rgb(var(--base));border-right:1px solid rgb(var(--base-200));box-shadow:rgba(var(--base),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{aspect-ratio:unset;width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(-180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;overflow:hidden;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid rgb(var(--contrast-200))}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}.main-actions .buttons.buttons{left:calc(var(--btn_) + .5rem)}
diff --git a/build/drawer-menu/style-index.css b/build/drawer-menu/style-index.css
index e97f166..0b1938a 100644
--- a/build/drawer-menu/style-index.css
+++ b/build/drawer-menu/style-index.css
@@ -1 +1 @@
-nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;right:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:rgb(var(--base));border-left:1px solid rgb(var(--base-200));box-shadow:rgba(var(--base),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid rgb(var(--contrast-200))}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}
+nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;right:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:rgb(var(--base));border-left:1px solid rgb(var(--base-200));box-shadow:rgba(var(--base),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{aspect-ratio:unset;width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;overflow:hidden;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid rgb(var(--contrast-200))}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}.main-actions .buttons.buttons{right:calc(var(--btn_) + .5rem)}
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 0e01911..c828fa8 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'b9d79a303cb5aad9e29a');
+<?php return array('dependencies' => array(), 'version' => 'c3aa1c027f932096017e');
diff --git a/build/feed/view.js b/build/feed/view.js
index a5dae15..a51b945 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},{timeout:2e3}):setTimeout(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map(e=>e.value):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)})}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(t=>t.dataset.taxonomy===e)[0]??null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach(t=>{t&&t.forEach(t=>{const i=t.dataset.for?.split(",")??[];t.hidden=i.length>0&&!i.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.orderbyWrap){let t=this.ui.orderbyWrap.dataset.forOrder.split(",")??[];this.ui.orderbyWrap.hidden=!t.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach(e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)})}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.orderbyFilters.filter(e=>!["date","modified","title","random"].includes(e)),t=[];e.forEach(e=>{t.push({name:e,keyPath:e})});const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe((e,t)=>{"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=!this.store.lastResponse?.has_more??!0))})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=e??this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),this.store.lastResponse?.has_more??!1)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){let s=i.images[t]??!1;s&&([e.src,e.srcset,e.alt]=[s.tiny,`${s.tiny} 50w, ${s.small} 300w, ${s.medium} 1024w`,s["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){let r=t.taxonomies[i][s]??!1;if(!r)continue;let a=o.cloneNode(!0),n=a.querySelector("a");if(!n)continue;let l=window.decodeHTMLEntities(r.title);[n.href,n.title,n.textContent]=[r.url,`See more ${l}`,l],e.append(a)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${r.fields.post_title??"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.feedBlock=new e)})})})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},{timeout:2e3}):setTimeout(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map(e=>e.value):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)})}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter(t=>t.dataset.taxonomy===e)[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach(t=>{t&&t.forEach(t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach(e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)})}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.orderbyFilters.filter(e=>!["date","modified","title","random"].includes(e)),t=[];e.forEach(e=>{t.push({name:e,keyPath:e})});const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe((e,t)=>{var i;"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i))})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=null!=e?e:this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{var i;this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(i=this.store.lastResponse?.has_more)&&void 0!==i&&i)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");if(!l)continue;let h=window.decodeHTMLEntities(r.title);[l.href,l.title,l.textContent]=[r.url,`See more ${h}`,h],e.append(n)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}var a;i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${null!==(a=r.fields.post_title)&&void 0!==a?a:"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.feedBlock=new e)})})})();
\ No newline at end of file
diff --git a/build/gmbreviews/render.php b/build/gmbreviews/render.php
index 487f394..779c1c4 100644
--- a/build/gmbreviews/render.php
+++ b/build/gmbreviews/render.php
@@ -57,7 +57,7 @@
 		ob_start();
 		?>
 		<div class="gmb-reviews">
-			<div class="row center">
+			<div class="row x-mid">
 			<?php
 			if ($showStats && !empty($average) && !empty($total)) {
 				?>
@@ -97,7 +97,7 @@
 			if ($showReviewLink && !empty($reviewUrl)) {
 				?>
 				<a href="<?=esc_url($reviewUrl)?>"
-				   class="button"
+				   class="btn"
 				   target="_blank"
 				   rel="noopener noreferrer">
 					<?= jvbIcon('star', ['style' => 'fill']) ?>
@@ -128,6 +128,7 @@
 				<li>
 					<blockquote class="review">
 						<?php
+						echo jvbIcon('quotes',['style' => 'fill']);
 						// Review text
 						if (!empty($comment)) { ?>
 							<div class="content review">
@@ -179,7 +180,7 @@
 			?>
 			<div class="footer">
 				<a href=" <?= esc_url($viewAllUrl) ?>"
-					class="button"
+					class="btn"
 					target="_blank"
 					rel="noopener noreferrer">
 
diff --git a/build/gmbreviews/style-index-rtl.css b/build/gmbreviews/style-index-rtl.css
index 65d99c0..f4dca2a 100644
--- a/build/gmbreviews/style-index-rtl.css
+++ b/build/gmbreviews/style-index-rtl.css
@@ -1 +1 @@
-.gmb-reviews{max-width:none}.gmb-reviews>.row.center{margin:0 auto;max-width:var(--content);--gap:.5rem 6rem}.gmb-reviews>.row.center p{width:-moz-fit-content;width:fit-content}.gmb-reviews .button{display:flex;height:-moz-max-content;height:max-content;margin:0 auto 2rem;width:66.6%}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:flex-start}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--full);padding:0}.gmb-reviews ul li{max-width:none;padding:4rem 1rem;width:100%}.gmb-reviews ul li:nth-of-type(odd){background-color:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(odd) blockquote{--background:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(2n){background-color:rgb(var(--base-100))}.gmb-reviews ul li:nth-of-type(2n) blockquote{--background:rgb(var(--base-100))}.gmb-reviews blockquote{margin:0 auto;max-width:var(--content);padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:rgb(var(--action-0))}.gmb-reviews article{background-color:rgb(var(--base));border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:rgb(var(--action-0))}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:none}.gmb-reviews>.row.x-mid{margin:0 auto;max-width:var(--content);--gap:.5rem 6rem}.gmb-reviews>.row.x-mid p{width:-moz-fit-content;width:fit-content}.gmb-reviews .button{display:flex;height:-moz-max-content;height:max-content;margin:0 auto 2rem;width:66.6%}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:flex-start}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--full);padding:0}.gmb-reviews ul li{max-width:none;padding:4rem 1rem;width:100%}.gmb-reviews ul li:nth-of-type(odd){background-color:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(odd) blockquote{--background:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(2n){background-color:rgb(var(--base-100))}.gmb-reviews ul li:nth-of-type(2n) blockquote{--background:rgb(var(--base-100))}.gmb-reviews blockquote{border-top:1px solid rgb(var(--base-200));margin:1rem auto;max-width:var(--content);padding:1rem 0 0}.gmb-reviews blockquote .icon-quotes-fi{right:calc(var(--btnbtn)*-1)}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{margin-top:var(--btn);position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:rgb(var(--action-0))}.gmb-reviews article{background-color:rgb(var(--base));border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:rgb(var(--action-0))}.gmb-reviews .btn{width:100%}
diff --git a/build/gmbreviews/style-index.css b/build/gmbreviews/style-index.css
index bfd106b..52b0a43 100644
--- a/build/gmbreviews/style-index.css
+++ b/build/gmbreviews/style-index.css
@@ -1 +1 @@
-.gmb-reviews{max-width:none}.gmb-reviews>.row.center{margin:0 auto;max-width:var(--content);--gap:.5rem 6rem}.gmb-reviews>.row.center p{width:-moz-fit-content;width:fit-content}.gmb-reviews .button{display:flex;height:-moz-max-content;height:max-content;margin:0 auto 2rem;width:66.6%}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:flex-start}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--full);padding:0}.gmb-reviews ul li{max-width:none;padding:4rem 1rem;width:100%}.gmb-reviews ul li:nth-of-type(odd){background-color:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(odd) blockquote{--background:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(2n){background-color:rgb(var(--base-100))}.gmb-reviews ul li:nth-of-type(2n) blockquote{--background:rgb(var(--base-100))}.gmb-reviews blockquote{margin:0 auto;max-width:var(--content);padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:rgb(var(--action-0))}.gmb-reviews article{background-color:rgb(var(--base));border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:rgb(var(--action-0))}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:none}.gmb-reviews>.row.x-mid{margin:0 auto;max-width:var(--content);--gap:.5rem 6rem}.gmb-reviews>.row.x-mid p{width:-moz-fit-content;width:fit-content}.gmb-reviews .button{display:flex;height:-moz-max-content;height:max-content;margin:0 auto 2rem;width:66.6%}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:flex-start}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--full);padding:0}.gmb-reviews ul li{max-width:none;padding:4rem 1rem;width:100%}.gmb-reviews ul li:nth-of-type(odd){background-color:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(odd) blockquote{--background:rgb(var(--base-50))}.gmb-reviews ul li:nth-of-type(2n){background-color:rgb(var(--base-100))}.gmb-reviews ul li:nth-of-type(2n) blockquote{--background:rgb(var(--base-100))}.gmb-reviews blockquote{border-top:1px solid rgb(var(--base-200));margin:1rem auto;max-width:var(--content);padding:1rem 0 0}.gmb-reviews blockquote .icon-quotes-fi{left:calc(var(--btnbtn)*-1)}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{margin-top:var(--btn);position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:rgb(var(--action-0))}.gmb-reviews article{background-color:rgb(var(--base));border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:rgb(var(--action-0))}.gmb-reviews .btn{width:100%}
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index fc8fe8f..0817591 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -113,7 +113,9 @@
 		{
 			//Ignore for both
 			$base = [
-				'core/null'
+				'core/null',
+				'core/list-item',
+				'jvb/drawer-menu'
 			];
 			if ($isPrerender) {
 				$base = array_merge($base, [
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index c521c15..10ffbaf 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -20,7 +20,6 @@
  */
 class ReferralManager
 {
-	protected $wpdb;
 	protected MagicLinkManager $magic_link;
 	protected Cache $cache;
 	protected Cache $requestCache;
@@ -37,12 +36,12 @@
 
 	// Default reward settings
 	protected array $default_settings = [
-		'referrer_reward_applies_to' => 'per_user',  // 'per_user' or 'flat_total'
-		'referrer_reward_amount' => 25.00,
-		'referrer_reward_type'	=> 'fixed',
-		'referee_reward_type' => 'percentage',  // 'percentage' or 'fixed'
-		'referee_reward_amount' => 20,  // 20% or $20
-		'referee_reward_applies_to' => 'first_order',  // 'first_order' or 'all_orders'
+		'from_user_reward_applies_to' => 'per_user',  // 'per_user' or 'flat_total'
+		'from_user_reward_amount' => 25.00,
+		'from_user_reward_type'	=> 'fixed',
+		'to_user_reward_type' => 'percentage',  // 'percentage' or 'fixed'
+		'to_user_reward_amount' => 20,  // 20% or $20
+		'to_user_reward_applies_to' => 'first_order',  // 'first_order' or 'all_orders'
 	];
 
 	protected string $role;
@@ -54,8 +53,7 @@
 		$this->defineTables();
 		$this->role = Site::getDefaultReferralRole();
 		$this->default_settings['referral_role'] = $this->role;
-		global $wpdb;
-		$this->wpdb = $wpdb;
+
 		$this->cache = Cache::for('referrals', WEEK_IN_SECONDS);
 		$this->requestCache = Cache::for('referral_requests', WEEK_IN_SECONDS)->connect('referrals', true);
 		$this->statsCache = Cache::for('referral_stats', WEEK_IN_SECONDS)->connect('referrals', true);
@@ -65,9 +63,6 @@
 			$this->statsCache->flush();
 		}
 
-		$this->referrals_table = $wpdb->prefix . BASE . 'referrals';
-		$this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards';
-
 		$this->referralPage = $this->getReferralPageId();
 		$this->settings = $this->getRewardSettings();
 
@@ -220,7 +215,7 @@
 				'id'				=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
 				'referral_id'		=> 'bigint(20) unsigned NOT NULL',
 				'user_id'			=> "{$table->getUserIDType()} NOT NULL",
-				'reward_type'		=> "ENUM('referrer', 'referee') NOT NULL",
+				'reward_type'		=> "ENUM('from_user', 'to_user') NOT NULL",
 				'amount'			=> 'decimal(10,2) NOT NULL',
 				'reward_calculation'=> "ENUM('percentage', 'fixed')",
 				'status'			=> "ENUM('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available'",
@@ -521,18 +516,24 @@
 			return false; // No referral code - regular registration
 		}
 
-		// Find the referrer
-		$referrer = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]);
-		if (empty($referrer)) {
+		// Find the from_user
+		$from_user = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]);
+		if (empty($from_user)) {
 			//This should not happen, but whatever
 			return false;
 		}
-		$referrer = $referrer[0];
+
+		// Check if this email already has a referral record
+		if ($this->isEmailInvited($referral['to_email'])) {
+			return false;
+		}
+
+		$from_user = $from_user[0];
 		$record = $this->referrals->findOrCreate([
 			'to_user'	=> $user_id,
 			'referral_code'	=> $referral['referral_code'],
 		], [
-			'from_user'		=> $referrer,
+			'from_user'		=> $from_user,
 			'to_email'		=> $referral['to_email'],
 			'to_name'		=> $userData['first_name'],
 //			'to_phone'		=>
@@ -558,39 +559,38 @@
 		$this->cache->flush();
 
 		// Fire action for tracking
-		do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']);
+		do_action('jvb_referral_processed', $user_id, $from_user->ID, $referral['referral_code']);
 
-		// Send notification to referrer
-		$this->sendReferrerNotification($referrer->ID, $userData['display_name']);
+		// Send notification to from_user
+		$this->sendReferrerNotification($from_user->ID, $userData['display_name']);
 		return true;
 	}
 
 	/**
 	 * Create a referral record in the database
 	 *
-	 * @param int $referrer_id
-	 * @param int $referee_id
+	 * @param int $from_user_id
+	 * @param int $to_user_id
 	 * @param string $code
 	 * @return int|false
 	 */
-	public function createReferral(int $referrer_id, int $referee_id, string $code)
+	public function createReferral(int $from_user_id, int $to_user_id, string $code)
 	{
-		$user = get_user_by('ID', $referee_id);
+		$user = get_user_by('ID', $to_user_id);
 
-		return $this->wpdb->insert(
-			$this->referrals_table,
+		return $this->referrals->findOrCreate([
 			[
-				'referrer_id' => $referrer_id,
-				'referee_id' => $referee_id,
-				'referee_name' => $user->display_name,
-				'referee_email' => $user->user_email,
-				'referee_phone' => get_user_meta($referee_id, BASE . 'phone', true) ?: '',
+				'to_user' => $to_user_id,
+			],
+			[
+				'from_user' => $from_user_id,
+				'to_name' => $user->display_name,
+				'to_email' => $user->user_email,
+				'to_phone' => get_user_meta($to_user_id, BASE . 'phone', true) ?: '',
 				'referral_code' => $code,
 				'status' => 'pending',
-				'referred_at' => current_time('mysql')
-			],
-			['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
-		);
+			]
+		]);
 	}
 
 	/**
@@ -615,17 +615,16 @@
 	}
 
 	/**
-	 * Get referral record by referee ID
+	 * Get referral record by to_user ID
 	 *
-	 * @param int $referee_id
+	 * @param int $to_user_id
 	 * @return object|null
 	 */
-	public function getReferralByReferee(int $referee_id): ?object
+	public function getReferralByReferee(int $to_user_id): ?object
 	{
-		$result = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT * FROM {$this->referrals_table} WHERE referee_id = %d",
-			$referee_id
-		));
+		$result = $this->referrals->get([
+			'to_user'	=> $to_user_id
+		]);
 
 		return $result ?: null;
 	}
@@ -641,15 +640,14 @@
 	{
 		$status = $treated ? 'treated' : 'pending';
 
-		$result = $this->wpdb->update(
-			$this->referrals_table,
+		$result = $this->referrals->update(
 			[
-				'status' => $status,
+				'status'	=> $status,
 				'treated_at' => $treated ? current_time('mysql') : null
 			],
-			['id' => $referral_id],
-			['%s', '%s'],
-			['%d']
+			[
+				'id'	=> $referral_id
+			]
 		);
 
 		if ($result && $treated) {
@@ -661,53 +659,41 @@
 	}
 
 	/**
-	 * Create reward records for both referrer and referee
+	 * Create reward records for both from_user and to_user
 	 *
 	 * @param int $referral_id
 	 */
 	protected function createRewardRecords(int $referral_id): void
 	{
-		$referral = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
-			$referral_id
-		));
+		$referral = $this->referrals->get(['id' => $referral_id]);
 
 		if (!$referral) {
 			return;
 		}
 
-		// Create referrer reward
-		$this->wpdb->insert(
-			$this->rewards_table,
-			[
-				'referral_id' => $referral_id,
-				'user_id' => $referral->referrer_id,
-				'reward_type' => 'referrer',
-				'amount' => $this->settings['referrer_reward_amount'],
-				'status' => 'available',
-				'created_at' => current_time('mysql')
-			],
-			['%d', '%d', '%s', '%f', '%s', '%s']
-		);
+		// Create from_user reward
+		$fromUserReward = $this->rewards->insert([
+			'referral_id'	=> $referral_id,
+			'user_id'		=> $referral->from_user,
+			'reward_type'	=> 'from_user',
+			'amount'		=> $this->settings['from_user_reward_amount'],
+			'reward_calculation' => $this->settings['from_user_reward_type']
+		]);
 
-		// Create referee reward
-		$referee_amount = $this->settings['referee_reward_type'] === 'percentage'
-			? $this->settings['referee_reward_amount']  // Store as percentage
-			: $this->settings['referee_reward_amount'];  // Store as fixed amount
 
-		$this->wpdb->insert(
-			$this->rewards_table,
-			[
-				'referral_id' => $referral_id,
-				'user_id' => $referral->referee_id,
-				'reward_type' => 'referee',
-				'amount' => $referee_amount,
-				'reward_calculation' => $this->settings['referee_reward_type'],
-				'status' => 'available',
-				'created_at' => current_time('mysql')
-			],
-			['%d', '%d', '%s', '%f', '%s', '%s', '%s']
-		);
+		// Create to_user reward
+		$to_user_amount = $this->settings['to_user_reward_type'] === 'percentage'
+			? $this->settings['to_user_reward_amount']  // Store as percentage
+			: $this->settings['to_user_reward_amount'];  // Store as fixed amount
+
+		$toUserReward = $this->rewards->insert([
+			'referral_id'	=> $referral_id,
+			'user_id'		=> $referral->to_user,
+			'reward_type'	=> 'to_user',
+			'amount'		=> $to_user_amount,
+			'reward_calculation' => $this->settings['to_user_reward_type']
+		]);
+
 	}
 
 	/**
@@ -723,47 +709,43 @@
 			'status' => 'all',
 			'limit' => 100,
 			'offset' => 0,
-			'orderby' => 'referred_at',
+			'orderby' => 'created_at',
 			'order' => 'DESC'
 		];
 
 		$args = wp_parse_args($args, $defaults);
+		$args['status'] = strtolower($args['status']);
+		$tableArgs = [
+			'where' => [
+				'from_user'	=> $user_id,
+			],
+			'limit'		=> $args['limit'],
+			'offset'	=> $args['offset'],
+			'orderby'	=> $args['orderby'],
+			'order'		=> $args['order']
+		];
+		if (in_array($args['status'], ['pending', 'consulted', 'treated', 'cancelled'])){
+			$tableArgs['status'] = $args['status'];
+		}
+		$referrals = $this->referrals->getMany($tableArgs);
 
-		return $this->requestCache->remember(
-			$this->requestCache->generateKey(array_merge(['user'=>$user_id], $args)),
-			function() use ($user_id, $args) {
-				$where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id);
-
-				if ($args['status'] !== 'all') {
-					$where .= $this->wpdb->prepare(" AND status = %s", $args['status']);
-				}
-
-				$query = "SELECT * FROM {$this->referrals_table}
-                  {$where}
-                  ORDER BY {$args['orderby']} {$args['order']}
-                  LIMIT {$args['limit']} OFFSET {$args['offset']}";
-
-				$results =  $this->wpdb->get_results($query);
-
-				return array_map(function($referral) {
-					$last_invite = get_transient('referral_last_invite_' . md5($referral->referee_email));
-					$can_resend = !$last_invite || (time() - $last_invite) > WEEK_IN_SECONDS;
-					$status = match($referral->status) {
-						'consulted' => 'Awaiting Treatment',
-						'treated'	=> 'Rewarded!',
-						default => 'Pending',
-					};
-					return [
-						'id'			=> $referral->id,
-						'referee_name'	=> $referral->referee_name,
-						'referee_email'	=> $referral->referee_email,
-						'referred_at'	=> JVB()->routes('referral')->formatTimestamp($referral->referred_at),
-						'referral_status'=> $status,
-						'can_resend'	=> $can_resend
-					];
-				}, $results);
-			}
-		);
+		return array_map(function($referral) {
+			$last_invite = get_transient('referral_last_invite_' . md5($referral->to_email));
+			$can_resend = !$last_invite || (time() - $last_invite) > WEEK_IN_SECONDS;
+			$status = match($referral->status) {
+				'consulted' => 'Awaiting Treatment',
+				'treated'	=> 'Rewarded!',
+				default => 'Pending',
+			};
+			return [
+				'id'			=> $referral->id,
+				'to_name'	=> $referral->to_name,
+				'to_email'	=> $referral->to_email,
+				'referred_at'	=> JVB()->routes('referral')->formatTimestamp($referral->referred_at),
+				'referral_status'=> $status,
+				'can_resend'	=> $can_resend
+			];
+		}, $referrals);
 
 	}
 
@@ -778,24 +760,23 @@
 		return $this->statsCache->remember(
 			$user_id,
 			function() use ($user_id) {
-				$stats = $this->wpdb->get_row($this->wpdb->prepare(
+				$stats = $this->referrals->queryResults(
 					"SELECT
-			COUNT(*) as code_used,
-			SUM(CASE WHEN status IN ('consulted', 'treated') THEN 1 ELSE 0 END) as consultations,
-			SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treatments,
-			SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending
-		FROM {$this->referrals_table}
-		WHERE referrer_id = %d",
-					$user_id
-				), ARRAY_A);
+                    COUNT(*) as code_used,
+                    SUM(CASE WHEN status IN ('consulted', 'treated') THEN 1 ELSE 0 END) as consultations,
+                    SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treatments,
+                    SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending
+                FROM {table}
+                WHERE from_user = %d",
+					[$user_id],
+					ARRAY_A
+				);
+				$stats = $stats[0] ?? [];
 
-				// Get total rewards earned (available + redeemed)
-				$rewards = $this->wpdb->get_var($this->wpdb->prepare(
-					"SELECT SUM(amount)
-		FROM {$this->rewards_table}
-		WHERE user_id = %d AND reward_type = 'referrer'",
-					$user_id
-				));
+				$rewards = $this->rewards->queryVar(
+					"SELECT SUM(amount) FROM {table} WHERE user_id = %d AND reward_type = 'from_user'",
+					[$user_id]
+				);
 
 				$stats['total_rewards'] = floatval($rewards ?? 0);
 				$stats['user_id'] = $user_id;
@@ -805,7 +786,7 @@
 	}
 
 	/**
-	 * Get top referrers for a time period
+	 * Get top from_users for a time period
 	 *
 	 * @param int $limit
 	 * @param string $period 'day'|'week'|'month'|'all'
@@ -814,44 +795,40 @@
 	public function getTopReferrers(int $limit = 10, string $period = 'all'): array
 	{
 		return $this->statsCache->remember(
-			$this->statsCache->generateKey(['limit'=>$limit, 'period' => $period]),
+			$this->statsCache->generateKey(['limit' => $limit, 'period' => $period]),
 			function() use ($limit, $period) {
 				$where = '';
-
 				if ($period !== 'all') {
-					$date_where = match($period) {
-						'day' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)",
-						'week' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)",
-						'month' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)",
+					$date_clause = match($period) {
+						'day'   => "created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)",
+						'week'  => "created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)",
+						'month' => "created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)",
 						default => "1=1"
 					};
-
-					$where = "WHERE {$date_where}";
+					$where = "WHERE {$date_clause}";
 				}
 
-				$query = "SELECT
-                    referrer_id,
+				$results = $this->referrals->queryResults(
+					"SELECT
+                    from_user,
                     COUNT(*) as referral_count,
                     SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count
-                  FROM {$this->referrals_table}
-                  {$where}
-                  GROUP BY referrer_id
-                  ORDER BY referral_count DESC
-                  LIMIT {$limit}";
+                FROM {table}
+                {$where}
+                GROUP BY from_user
+                ORDER BY referral_count DESC
+                LIMIT {$limit}"
+				);
 
-				$results = $this->wpdb->get_results($query);
-
-				// Enrich with user data
 				foreach ($results as &$result) {
-					$user = get_user_by('ID', $result->referrer_id);
-					$result->user_name = $user ? $user->display_name : 'Unknown';
-					$result->user_email = $user ? $user->user_email : '';
+					$user = get_user_by('ID', $result->from_user);
+					$result->user_name  = $user ? $user->display_name : 'Unknown';
+					$result->user_email = $user ? $user->user_email   : '';
 				}
 
 				return $results;
 			}
 		);
-
 	}
 
 	/**
@@ -861,14 +838,14 @@
 	{
 		$yesterday = date('Y-m-d', strtotime('-1 day'));
 
-		$new_referrals = $this->wpdb->get_results($this->wpdb->prepare(
-			"SELECT r.*, u.display_name as referrer_name
-        FROM {$this->referrals_table} r
-        JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
-        WHERE DATE(r.referred_at) = %s
-        ORDER BY r.referred_at DESC",
-			$yesterday
-		));
+		$new_referrals = $this->referrals->queryResults(
+			"SELECT {table}.*, u.display_name as from_user_name
+        FROM {table}
+        JOIN {$this->referrals->getUserTable()} u ON {table}.from_user = u.ID
+        WHERE DATE({table}.created_at) = %s
+        ORDER BY {table}.created_at DESC",
+			[$yesterday]
+		);
 
 		if (empty($new_referrals)) {
 			return;
@@ -888,13 +865,13 @@
 		foreach ($new_referrals as $ref) {
 			$cardContent = sprintf(
 				'<p><strong>%s</strong> (%s)</p>',
-				esc_html($ref->referee_name),
-				esc_html($ref->referee_email)
+				esc_html($ref->to_name),
+				esc_html($ref->to_email)
 			);
 			$cardContent .= sprintf(
 				'<p style="font-size:13px;color:%s;">Referred by: %s | Code: %s</p>',
 				JVB()->email()->colours['dark-200'],
-				esc_html($ref->referrer_name),
+				esc_html($ref->from_name),
 				JVB()->email()->badge($ref->referral_code, 'info')
 			);
 
@@ -913,14 +890,13 @@
 	}
 
 	/**
-	 * Send weekly report with top referrers
+	 * Send weekly report with top from_users
 	 */
 	public function sendWeeklyReport(): void
 	{
-		$top_referrers = $this->getTopReferrers(10, 'week');
-		$total_referrals = $this->wpdb->get_var(
-			"SELECT COUNT(*) FROM {$this->referrals_table}
-         WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)"
+		$top_from_users = $this->getTopReferrers(10, 'week');
+		$total_referrals = $this->referrals->queryVar(
+			"SELECT COUNT(*) FROM {table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)"
 		);
 
 		if ($total_referrals == 0) {
@@ -939,7 +915,7 @@
 
 		// Leaderboard style
 		$rank = 1;
-		foreach ($top_referrers as $referrer) {
+		foreach ($top_from_users as $from_user) {
 			$rankBadge = $rank <= 3
 				? JVB()->email()->badge('#' . $rank, $rank === 1 ? 'success' : 'info')
 				: '<span style="font-weight:600;color:' . JVB()->email()->colours['dark-200'] . ';">#' . $rank . '</span>';
@@ -947,12 +923,12 @@
 			$cardContent = sprintf(
 				'<p>%s <strong>%s</strong></p>',
 				$rankBadge,
-				esc_html($referrer->user_name)
+				esc_html($from_user->user_name)
 			);
 
 			$stats = [
-				JVB()->email()->stat($referrer->referral_count, 'Total Referrals'),
-				JVB()->email()->stat($referrer->treated_count, 'Treated')
+				JVB()->email()->stat($from_user->referral_count, 'Total Referrals'),
+				JVB()->email()->stat($from_user->treated_count, 'Treated')
 			];
 			$cardContent .= JVB()->email()->grid($stats, 2);
 
@@ -983,10 +959,10 @@
 				foreach ($referrals as $referral) {
 					$csv .= sprintf(
 						'"%s","%s","%s","%s","%s","%s","%s","%s"' . "\n",
-						$referral->referrer_name ?? 'Unknown',
-						$referral->referee_name,
-						$referral->referee_email,
-						$referral->referee_phone,
+						$referral->from_name ?? 'Unknown',
+						$referral->to_name,
+						$referral->to_email,
+						$referral->to_phone,
 						$referral->referral_code,
 						$referral->status,
 						$referral->referred_at,
@@ -1003,11 +979,11 @@
 	/**
 	 * Generate HTML email for weekly report
 	 *
-	 * @param array $top_referrers
+	 * @param array $top_from_users
 	 * @param int $total_referrals
 	 * @return string
 	 */
-	protected function generateWeeklyReportEmail(array $top_referrers, int $total_referrals): string
+	protected function generateWeeklyReportEmail(array $top_from_users, int $total_referrals): string
 	{
 		$content = sprintf(
 			'<p>This week you had <strong>%d total referral%s</strong>.</p>',
@@ -1015,20 +991,20 @@
 			$total_referrals !== 1 ? 's' : ''
 		);
 
-		$referrers = [];
+		$from_users = [];
 		$rank = 1;
-		foreach ($top_referrers as $referrer) {
-			$referrers[] = [
-				'label' => '#' . $rank++ . ' - ' . esc_html($referrer->user_name),
+		foreach ($top_from_users as $from_user) {
+			$from_users[] = [
+				'label' => '#' . $rank++ . ' - ' . esc_html($from_user->user_name),
 				'value' => sprintf(
 					'<strong>Total Referrals:</strong> %d | <strong>Treated:</strong> %d',
-					$referrer->referral_count,
-					$referrer->treated_count
+					$from_user->referral_count,
+					$from_user->treated_count
 				)
 			];
 		}
 
-		$content .= JVB()->email()->table($referrers, 'Top 10 Referrers This Week');
+		$content .= JVB()->email()->table($from_users, 'Top 10 Referrers This Week');
 
 		return $content;
 	}
@@ -1119,8 +1095,8 @@
 			<tbody>
 			<?php foreach ($referrals as $referral): ?>
 				<tr>
-					<td><?php echo esc_html($referral->referee_name); ?></td>
-					<td><?php echo esc_html($referral->referee_email); ?></td>
+					<td><?php echo esc_html($referral->to_name); ?></td>
+					<td><?php echo esc_html($referral->to_email); ?></td>
 					<td><?php echo esc_html(ucfirst($referral->status)); ?></td>
 					<td><?php echo esc_html($referral->referred_at); ?></td>
 					<td>
@@ -1250,17 +1226,17 @@
 
 		// Pre-fill code if from referral link
 		$prefill_code = $_GET['ref'] ?? '';
-		$referrer_name = '';
+		$from_user_name = '';
 		if ($prefill_code) {
-			$referrer = $this->getUserByReferralCode($prefill_code);
-			$referrer_name = $referrer ? strtok($referrer->display_name, ' ') : '';
+			$from_user = $this->getUserByReferralCode($prefill_code);
+			$from_user_name = $from_user ? strtok($from_user->display_name, ' ') : '';
 		}
 
 		$header = sprintf(
 			'<header><h2>%sGet %s.</h2></header><h3>Have a code?</h3>%s<p>Enter your referral code to get started!</p>',
 			jvbIcon('confetti'),
 			esc_html($reward_text),
-			($referrer_name ? '<p>' . esc_html($referrer_name) . ' invited you to join us</p>' : '')
+			($from_user_name ? '<p>' . esc_html($from_user_name) . ' invited you to join us</p>' : '')
 		);
 
 		$codeForm = sprintf(
@@ -1311,7 +1287,7 @@
 				'pattern'	=> '[A-Za-z0-9]+',
 				'maxLength'	=> 20,
 				'autocomplete'=>'off',
-				'data-referrer' => $referrer_name
+				'data-from-user' => $from_user_name
 			]),
 			$turnstile,
 			jvbIcon('check-circle')
@@ -1375,14 +1351,14 @@
 
 	protected function getReferralSuccessMessage(string $code): string
 	{
-		$referrer = $this->getUserByReferralCode($code);
+		$from_user = $this->getUserByReferralCode($code);
 
-		if (!$referrer) {
+		if (!$from_user) {
 			return '';
 		}
 
-		$reward_amount = $this->settings['referee_reward_amount'] ?? 20;
-		$reward_type = $this->settings['referee_reward_type'] ?? 'percentage';
+		$reward_amount = $this->settings['to_user_reward_amount'] ?? 20;
+		$reward_type = $this->settings['to_user_reward_type'] ?? 'percentage';
 
 		$reward_text = $reward_type === 'percentage'
 			? $reward_amount . '% off'
@@ -1411,7 +1387,7 @@
 			</a>
 
 			<div class="referred-by">
-				Referred by <strong><?php echo esc_html($referrer->display_name); ?></strong>
+				Referred by <strong><?php echo esc_html($from_user->display_name); ?></strong>
 			</div>
 		</div>
 		<?php
@@ -1506,8 +1482,8 @@
 			return $content;
 		}
 
-		$reward_amount = $this->settings['referee_reward_amount'] ?? 20;
-		$reward_type = $this->settings['referee_reward_type'] ?? 'percentage';
+		$reward_amount = $this->settings['to_user_reward_amount'] ?? 20;
+		$reward_type = $this->settings['to_user_reward_type'] ?? 'percentage';
 
 		$reward_text = $reward_type === 'percentage'
 			? $reward_amount . '% off'
@@ -1582,8 +1558,8 @@
 			return new WP_Error('user_exists', 'This person already has an account');
 		}
 
-		// Get referrer info
-		$referrer = get_user_by('ID', $user_id);
+		// Get from_user info
+		$from_user = get_user_by('ID', $user_id);
 		$referral_code = $this->getUserReferralCode($user_id);
 
 		if ($referral_code) {
@@ -1609,9 +1585,9 @@
 		], home_url('/'));
 
 		// Get reward text for email
-		$reward_text = $this->settings['referee_reward_type'] === 'percentage'
-			? "{$this->settings['referee_reward_amount']}% off"
-			: "\${$this->settings['referee_reward_amount']} off";
+		$reward_text = $this->settings['to_user_reward_type'] === 'percentage'
+			? "{$this->settings['to_user_reward_amount']}% off"
+			: "\${$this->settings['to_user_reward_amount']} off";
 
 		// Build email content
 		$email_content =
@@ -1624,7 +1600,7 @@
 			<p>Click the button below to register and claim your reward:</p>
 			%s
 			<p><small>This invitation expires in 30 days.</small></p>',
-				esc_html($referrer->display_name),
+				esc_html($from_user->display_name),
 				esc_html(get_bloginfo('name')),
 				nl2br(esc_html($message)),
 				esc_html($reward_text),
@@ -1743,10 +1719,7 @@
 		}
 
 		// Check if there's a pending referral for this email
-		$existing = $this->wpdb->get_var($this->wpdb->prepare(
-			"SELECT id FROM {$this->referrals_table} WHERE referee_email = %s",
-			$email
-		));
+		$existing = $this->referrals->pluck('id', ['to_email' => $email]);
 
 		return !empty($existing);
 	}
@@ -1818,25 +1791,24 @@
 	 */
 	public function exportReferrals(string $start_date, string $end_date): string
 	{
-		$referrals = $this->wpdb->get_results($this->wpdb->prepare(
+		$referrals = $this->referrals->queryResults(
 			"SELECT
-            r.id,
-            r.referee_name,
-            r.referee_email,
-            r.referee_phone,
-            r.referral_code,
-            r.referred_at,
-            r.status,
-            r.treated_at,
-            u.display_name as referrer_name,
-            u.user_email as referrer_email
-        FROM {$this->referrals_table} r
-        JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
-        WHERE DATE(r.referred_at) BETWEEN %s AND %s
-        ORDER BY r.referred_at DESC",
-			$start_date,
-			$end_date
-		));
+            {table}.id,
+            {table}.to_name,
+            {table}.to_email,
+            {table}.to_phone,
+            {table}.referral_code,
+            {table}.created_at,
+            {table}.status,
+            {table}.treated_at,
+            u.display_name as from_name,
+            u.user_email as from_email
+        FROM {table}
+        JOIN {$this->referrals->getUserTable()} u ON {table}.from_user = u.ID
+        WHERE DATE({table}.created_at) BETWEEN %s AND %s
+        ORDER BY {table}.created_at DESC",
+			[$start_date, $end_date]
+		);
 
 		// Build CSV
 		$csv_lines = [];
@@ -1859,15 +1831,15 @@
 		foreach ($referrals as $ref) {
 			$csv_lines[] = [
 				$ref->id,
-				$ref->referee_name,
-				$ref->referee_email,
-				$ref->referee_phone ?: 'N/A',
+				$ref->to_name,
+				$ref->to_email,
+				$ref->to_phone ?: 'N/A',
 				$ref->referral_code,
 				$ref->referred_at,
 				ucfirst($ref->status),
 				$ref->treated_at ?: 'N/A',
-				$ref->referrer_name,
-				$ref->referrer_email
+				$ref->from_name,
+				$ref->from_email
 			];
 		}
 
@@ -1944,19 +1916,19 @@
 	public function sanitizeRewardSettings(array $settings): array
 	{
 		return [
-			'referrer_reward_applies_to' => in_array($settings['referrer_reward_applies_to'] ?? '', ['per_user', 'flat_total'])
-				? $settings['referrer_reward_applies_to']
+			'from_user_reward_applies_to' => in_array($settings['from_user_reward_applies_to'] ?? '', ['per_user', 'flat_total'])
+				? $settings['from_user_reward_applies_to']
 				: 'per_user',
-			'referrer_reward_amount' => floatval($settings['referrer_reward_amount'] ?? 25.00),
-			'referrer_reward_type' => in_array($settings['referrer_reward_type'] ?? '', ['fixed', 'percentage'])
-				? $settings['referrer_reward_type']
+			'from_user_reward_amount' => floatval($settings['from_user_reward_amount'] ?? 25.00),
+			'from_user_reward_type' => in_array($settings['from_user_reward_type'] ?? '', ['fixed', 'percentage'])
+				? $settings['from_user_reward_type']
 				: 'fixed',
-			'referee_reward_type' => in_array($settings['referee_reward_type'] ?? '', ['percentage', 'fixed'])
-				? $settings['referee_reward_type']
+			'to_user_reward_type' => in_array($settings['to_user_reward_type'] ?? '', ['percentage', 'fixed'])
+				? $settings['to_user_reward_type']
 				: 'percentage',
-			'referee_reward_amount' => floatval($settings['referee_reward_amount'] ?? 20),
-			'referee_reward_applies_to' => in_array($settings['referee_reward_applies_to'] ?? '', ['first_order', 'all_orders'])
-				? $settings['referee_reward_applies_to']
+			'to_user_reward_amount' => floatval($settings['to_user_reward_amount'] ?? 20),
+			'to_user_reward_applies_to' => in_array($settings['to_user_reward_applies_to'] ?? '', ['first_order', 'all_orders'])
+				? $settings['to_user_reward_applies_to']
 				: 'first_order',
 		];
 	}
@@ -2246,12 +2218,12 @@
 					} else {
 						data.items.forEach(function(ref) {
 							html += '<tr>';
-							html += '<td>' + (ref.referrer_name || 'Unknown') + '</td>';
-							html += '<td>' + (ref.referee_display_name || ref.referee_name) + '</td>';
-							html += '<td>' + (ref.referee_display_email || ref.referee_email) + '</td>';
+							html += '<td>' + (ref.from_name || 'Unknown') + '</td>';
+							html += '<td>' + (ref.to_user_display_name || ref.to_name) + '</td>';
+							html += '<td>' + (ref.to_user_display_email || ref.to_email) + '</td>';
 							html += '<td><span class="referral-status ' + ref.status + '">' + ref.status + '</span></td>';
 							html += '<td>' + new Date(ref.referred_at).toLocaleDateString() + '</td>';
-							html += '<td>' + (ref.referrer_total_referrals || 0) + '</td>';
+							html += '<td>' + (ref.from_user_total_referrals || 0) + '</td>';
 							html += '<td class="referral-actions">';
 
 							if (ref.status === 'pending') {
@@ -2408,24 +2380,24 @@
 						</tr>
 						<tr>
 							<th scope="row">
-								<label for="referrer_reward_type">Reward Type</label>
+								<label for="from_user_reward_type">Reward Type</label>
 							</th>
 							<td>
-								<select name="referrer_reward_type" id="referrer_reward_type">
-									<option value="fixed" <?php selected($this->settings['referrer_reward_type'], 'fixed'); ?>>Fixed Amount</option>
-									<option value="percentage" <?php selected($this->settings['referrer_reward_type'], 'percentage'); ?>>Percentage</option>
+								<select name="from_user_reward_type" id="from_user_reward_type">
+									<option value="fixed" <?php selected($this->settings['from_user_reward_type'], 'fixed'); ?>>Fixed Amount</option>
+									<option value="percentage" <?php selected($this->settings['from_user_reward_type'], 'percentage'); ?>>Percentage</option>
 								</select>
 							</td>
 						</tr>
 						<tr>
 							<th scope="row">
-								<label for="referrer_reward_amount">Reward Amount</label>
+								<label for="from_user_reward_amount">Reward Amount</label>
 							</th>
 							<td>
 								<input type="number"
-									   name="referrer_reward_amount"
-									   id="referrer_reward_amount"
-									   value="<?= esc_attr($this->settings['referrer_reward_amount']) ?>"
+									   name="from_user_reward_amount"
+									   id="from_user_reward_amount"
+									   value="<?= esc_attr($this->settings['from_user_reward_amount']) ?>"
 									   step="0.01"
 									   min="0">
 								<p class="description">Amount in dollars or percentage</p>
@@ -2433,12 +2405,12 @@
 						</tr>
 						<tr>
 							<th scope="row">
-								<label for="referrer_reward_applies_to">Applies To</label>
+								<label for="from_user_reward_applies_to">Applies To</label>
 							</th>
 							<td>
-								<select name="referrer_reward_applies_to" id="referrer_reward_applies_to">
-									<option value="per_user" <?php selected($this->settings['referrer_reward_applies_to'], 'per_user'); ?>>Per User Referred</option>
-									<option value="flat_total" <?php selected($this->settings['referrer_reward_applies_to'], 'flat_total'); ?>>Flat Total</option>
+								<select name="from_user_reward_applies_to" id="from_user_reward_applies_to">
+									<option value="per_user" <?php selected($this->settings['from_user_reward_applies_to'], 'per_user'); ?>>Per User Referred</option>
+									<option value="flat_total" <?php selected($this->settings['from_user_reward_applies_to'], 'flat_total'); ?>>Flat Total</option>
 								</select>
 							</td>
 						</tr>
@@ -2448,24 +2420,24 @@
 						</tr>
 						<tr>
 							<th scope="row">
-								<label for="referee_reward_type">Reward Type</label>
+								<label for="to_user_reward_type">Reward Type</label>
 							</th>
 							<td>
-								<select name="referee_reward_type" id="referee_reward_type">
-									<option value="percentage" <?php selected($this->settings['referee_reward_type'], 'percentage'); ?>>Percentage</option>
-									<option value="fixed" <?php selected($this->settings['referee_reward_type'], 'fixed'); ?>>Fixed Amount</option>
+								<select name="to_user_reward_type" id="to_user_reward_type">
+									<option value="percentage" <?php selected($this->settings['to_user_reward_type'], 'percentage'); ?>>Percentage</option>
+									<option value="fixed" <?php selected($this->settings['to_user_reward_type'], 'fixed'); ?>>Fixed Amount</option>
 								</select>
 							</td>
 						</tr>
 						<tr>
 							<th scope="row">
-								<label for="referee_reward_amount">Reward Amount</label>
+								<label for="to_user_reward_amount">Reward Amount</label>
 							</th>
 							<td>
 								<input type="number"
-									   name="referee_reward_amount"
-									   id="referee_reward_amount"
-									   value="<?= esc_attr($this->settings['referee_reward_amount']) ?>"
+									   name="to_user_reward_amount"
+									   id="to_user_reward_amount"
+									   value="<?= esc_attr($this->settings['to_user_reward_amount']) ?>"
 									   step="0.01"
 									   min="0">
 								<p class="description">Amount in dollars or percentage</p>
@@ -2473,12 +2445,12 @@
 						</tr>
 						<tr>
 							<th scope="row">
-								<label for="referee_reward_applies_to">Applies To</label>
+								<label for="to_user_reward_applies_to">Applies To</label>
 							</th>
 							<td>
-								<select name="referee_reward_applies_to" id="referee_reward_applies_to">
-									<option value="first_order" <?php selected($this->settings['referee_reward_applies_to'], 'first_order'); ?>>First Order Only</option>
-									<option value="all_orders" <?php selected($this->settings['referee_reward_applies_to'], 'all_orders'); ?>>All Orders</option>
+								<select name="to_user_reward_applies_to" id="to_user_reward_applies_to">
+									<option value="first_order" <?php selected($this->settings['to_user_reward_applies_to'], 'first_order'); ?>>First Order Only</option>
+									<option value="all_orders" <?php selected($this->settings['to_user_reward_applies_to'], 'all_orders'); ?>>All Orders</option>
 								</select>
 							</td>
 						</tr>
@@ -2798,11 +2770,11 @@
 			->content('referral', 'Referral', 'Referrals')
 //			->initMeta('custom', 'referral')
 			->setFields([
-				'referee_name' => [
+				'to_name' => [
 					'label' => 'Name',
 					'type' => 'text',
 				],
-				'referee_email' => [
+				'to_email' => [
 					'label' => 'Email',
 					'type' => 'text',
 				],
@@ -2876,12 +2848,12 @@
 
 			// Save reward settings
 			$settings = [
-				'referrer_reward_type' => sanitize_text_field($post_data['referrer_reward_type'] ?? 'fixed'),
-				'referrer_reward_amount' => floatval($post_data['referrer_reward_amount'] ?? 25.00),
-				'referrer_reward_applies_to' => sanitize_text_field($post_data['referrer_reward_applies_to'] ?? 'per_user'),
-				'referee_reward_type' => sanitize_text_field($post_data['referee_reward_type'] ?? 'percentage'),
-				'referee_reward_amount' => floatval($post_data['referee_reward_amount'] ?? 20),
-				'referee_reward_applies_to' => sanitize_text_field($post_data['referee_reward_applies_to'] ?? 'first_order')
+				'from_user_reward_type' => sanitize_text_field($post_data['from_user_reward_type'] ?? 'fixed'),
+				'from_user_reward_amount' => floatval($post_data['from_user_reward_amount'] ?? 25.00),
+				'from_user_reward_applies_to' => sanitize_text_field($post_data['from_user_reward_applies_to'] ?? 'per_user'),
+				'to_user_reward_type' => sanitize_text_field($post_data['to_user_reward_type'] ?? 'percentage'),
+				'to_user_reward_amount' => floatval($post_data['to_user_reward_amount'] ?? 20),
+				'to_user_reward_applies_to' => sanitize_text_field($post_data['to_user_reward_applies_to'] ?? 'first_order')
 			];
 
 			update_option(BASE . 'referral_settings', $settings);
@@ -2900,15 +2872,15 @@
 	}
 
 	/**
-	 * Get formatted reward text for referee
+	 * Get formatted reward text for to_user
 	 *
 	 * @param bool $full Include "off your first treatment" text
 	 * @return string
 	 */
 	public function getRewardText(bool $full = true): string
 	{
-		$reward_amount = $this->settings['referee_reward_amount'] ?? 20;
-		$reward_type = $this->settings['referee_reward_type'] ?? 'percentage';
+		$reward_amount = $this->settings['to_user_reward_amount'] ?? 20;
+		$reward_type = $this->settings['to_user_reward_type'] ?? 'percentage';
 
 		$reward_text = $reward_type === 'percentage'
 			? $reward_amount . '% off'
@@ -2973,28 +2945,28 @@
 	}
 
 	/**
-	 * Send notification to referrer when someone registers
+	 * Send notification to from_user when someone registers
 	 *
-	 * @param int $referrer_id
-	 * @param string $referee_name
+	 * @param int $from_user_id
+	 * @param string $to_name
 	 */
-	protected function sendReferrerNotification(int $referrer_id, string $referee_name): void
+	protected function sendReferrerNotification(int $from_user_id, string $to_name): void
 	{
-		$referrer = get_userdata($referrer_id);
-		if (!$referrer) {
+		$from_user = get_userdata($from_user_id);
+		if (!$from_user) {
 			return;
 		}
 
-		$subject = sprintf('%s signed up with your referral code!', $referee_name);
+		$subject = sprintf('%s signed up with your referral code!', $to_name);
 		$message = sprintf(
 			"Great news! %s just signed up using your referral code.\n\n" .
 			"View your referrals: %s",
-			$referee_name,
+			$to_name,
 			home_url('/dash/referrals')
 		);
 
 		JVB()->email()->sendEmail(
-			$referrer->user_email,
+			$from_user->user_email,
 			$subject,
 			$message
 		);
@@ -3021,9 +2993,9 @@
 			return '';
 		}
 
-		// Get referrer name
-		$referrer = get_userdata($referral->referrer_id);
-		$referrer_first_name = $referrer ? strtok($referrer->display_name, ' ') : 'Your friend';
+		// Get from_user name
+		$from_user = get_userdata($referral->from_user);
+		$from_user_first_name = $from_user ? strtok($from_user->display_name, ' ') : 'Your friend';
 
 		// Get reward text
 		$reward_text = $this->getRewardText(); // Just "20% off" or "$25 off"
@@ -3035,7 +3007,7 @@
 		?>
 		<div class="welcome-banner referral-welcome">
 			<div class="banner-content">
-				<h3><?= jvbIcon('confetti') ?>Welcome! <small><b><?= esc_html($referrer_first_name) ?></b> invited you to save <b><?= esc_html($reward_text) ?></b>!</small></h3>
+				<h3><?= jvbIcon('confetti') ?>Welcome! <small><b><?= esc_html($from_user_first_name) ?></b> invited you to save <b><?= esc_html($reward_text) ?></b>!</small></h3>
 				<p>But we're not done yet! Here's what happens next:</p>
 				<div class="callout">
 					<ol>
@@ -3064,5 +3036,118 @@
 		<?php
 		return ob_get_clean();
 	}
+
+	public function updateStatus(int $referral_id, string $status): bool|WP_Error
+	{
+		$referral = $this->referrals->get(['id' => $referral_id]);
+		if (!$referral) {
+			return new WP_Error('not_found', 'Referral not found');
+		}
+
+		$data = ['status' => $status, "{$status}_at" => current_time('mysql')];
+		if ($status === 'treated') {
+			$data['treatment_count'] = ($referral->treatment_count ?? 0) + 1;
+		}
+
+		$result = $this->referrals->update($data, ['id' => $referral_id]);
+		if ($result === false) {
+			return new WP_Error('update_failed', 'Failed to update referral status');
+		}
+
+		if ($status === 'treated') {
+			$this->createRewardRecords($referral_id);
+		}
+
+		$this->cache->flush();
+		return true;
+	}
+
+	public function removeReferral(int $referral_id, int $user_id): bool|WP_Error
+	{
+		$referral = $this->referrals->get(['id' => $referral_id]);
+		if (!$referral) {
+			return new WP_Error('not_found', 'Referral not found');
+		}
+
+		if ($referral->from_user != $user_id && !current_user_can('manage_options')) {
+			return new WP_Error('unauthorized', 'Unauthorized');
+		}
+
+		if ($referral->status !== 'pending') {
+			return new WP_Error('invalid_status', 'Can only remove pending referrals');
+		}
+
+		$this->referrals->delete(['id' => $referral_id]);
+		$this->cache->flush();
+		return true;
+	}
+
+	public function resendInvitation(int $referral_id, int $user_id): bool|WP_Error
+	{
+		$referral = $this->referrals->where(['id' => $referral_id, 'from_user' => $user_id])->first();
+		if (!$referral) {
+			return new WP_Error('not_found', 'Referral not found');
+		}
+
+		$transient_key = 'referral_last_invite_' . md5($referral->to_email);
+		if (get_transient($transient_key)) {
+			return new WP_Error('rate_limit', 'Can only resend once per week');
+		}
+
+		$result = $this->sendReferralInvitation(
+			$user_id,
+			$referral->to_email,
+			$referral->to_name,
+			sprintf('Reminder: Join %s', get_bloginfo('name')),
+			'Just a friendly reminder about my invitation!'
+		);
+
+		if (is_wp_error($result)) {
+			return $result;
+		}
+
+		set_transient($transient_key, time(), WEEK_IN_SECONDS);
+		return true;
+	}
+
+	public function getAllReferrals(array $args = []): array
+	{
+		$conditions = ['1=1'];
+		$values = [];
+
+		if (!empty($args['status']) && $args['status'] !== 'all') {
+			$conditions[] = '{table}.status = %s';
+			$values[] = $args['status'];
+		}
+
+		if (!empty($args['date_start'])) {
+			$conditions[] = 'DATE({table}.created_at) >= %s';
+			$values[] = $args['date_start'];
+		}
+
+		if (!empty($args['date_end'])) {
+			$conditions[] = 'DATE({table}.created_at) <= %s';
+			$values[] = $args['date_end'];
+		}
+
+		if (!empty($args['search'])) {
+			global $wpdb;
+			$like = '%' . $wpdb->esc_like($args['search']) . '%';
+			$conditions[] = '({table}.to_name LIKE %s OR {table}.to_email LIKE %s OR {table}.referral_code LIKE %s OR u.display_name LIKE %s)';
+			array_push($values, $like, $like, $like, $like);
+		}
+
+		array_push($values, absint($args['limit'] ?? 50), absint($args['offset'] ?? 0));
+
+		return $this->referrals->queryResults(
+			"SELECT {table}.*, u.display_name as from_name
+        FROM {table}
+        LEFT JOIN {$this->referrals->getUserTable()} u ON {table}.from_user = u.ID
+        WHERE " . implode(' AND ', $conditions) . "
+        ORDER BY {table}.created_at DESC
+        LIMIT %d OFFSET %d",
+			$values
+		);
+	}
 }
 
diff --git a/inc/managers/SEO/render/Traits/_Properties/reviewTrait.php b/inc/managers/SEO/render/Traits/_Properties/reviewTrait.php
index db6bcf1..4c677b7 100644
--- a/inc/managers/SEO/render/Traits/_Properties/reviewTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/reviewTrait.php
@@ -74,7 +74,7 @@
 			'enable_gmb_review_sync',
 			[
 				'type'	=> 'true_false',
-				'label'	=> __('Enable Google My Business Sync', 'jvb'),
+				'label'	=> 'Enable Google My Business Sync',
 				'hint'	=> 'Automagically fetch your latest positive reviews from your Google Business Listing.
 				Note: you must enable your GMB integration in settings.',
 			]
@@ -98,27 +98,27 @@
 			'reviews',
 			[
 				'type'	=> 'repeater',
-				'label'	=> __('Reviews', 'jvb'),
+				'label'	=> 'Reviews',
 				'fields'	=> [
 					'author'	=> [
 						'type'	=> 'text',
-						'label'	=> __('Review Author', 'jvb'),
+						'label'	=> 'Review Author',
 						'required'	=> true,
 					],
 					'reviewRating'	=> [
 						'type'	=> 'number',
 						'min'	=> 1,
 						'max'	=> 5,
-						'label'	=> __('Review Rating', 'jvb'),
+						'label'	=> 'Review Rating',
 						'required'	=> true,
 					],
 					'reviewBody'	=> [
 						'type'	=> 'textarea',
-						'label'	=> __('Review Text Content', 'jvb'),
+						'label'	=> 'Review Text Content',
 					],
 					'url'		=> [
 						'type'	=> 'url',
-						'label'	=> __('Link to actual review'),
+						'label'	=> 'Link to actual review',
 						'hint'	=> 'Optional. Link directly to the review source for added authenticity.'
 					]
 				],
diff --git a/inc/managers/SEO/render/seo/_setup.php b/inc/managers/SEO/render/seo/_setup.php
deleted file mode 100644
index cd1edf4..0000000
--- a/inc/managers/SEO/render/seo/_setup.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
-require(JVB_DIR . '/inc/managers/SEO/render/Output/Archive.php');
-require(JVB_DIR . '/inc/managers/SEO/render/Output/Meta.php');
-require(JVB_DIR . '/inc/managers/SEO/render/Output/Resolver.php');
-require(JVB_DIR . '/inc/managers/SEO/render/Output/Schema.php');
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index 593a8c6..81c78da 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -142,12 +142,12 @@
 	/**
 	 * @var bool Whether users/content need to request the owner for admission
 	 */
-	protected bool $verify_entry;
+	protected bool $verify_entry = false;
 	protected ?MakeVerification $verifyEntryHandler = null;
 	/**
 	 * @var bool Whether we should track post movements from term to term (ie. artists in tattoo shops)
 	 */
-	protected bool $track_changes;
+	protected bool $track_changes = false;
 	protected ?MakeTrackChanges $trackChangesHandler = null;
 
 	/**
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index 2ef284a..a1e3f3e 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -20,20 +20,12 @@
  */
 class ReferralRoutes extends Rest
 {
-	protected CustomTable $referrals;
-	protected CustomTable $rewards;
-	protected CustomTable $treatments;
-
 	public function __construct()
 	{
 		$this->cacheName = 'referrals';
 		$this->cacheTtl = (int)HOUR_IN_SECONDS;
 		parent::__construct();
 
-		$this->referrals = CustomTable::for('referrals');
-		$this->rewards = CustomTable::for('referral_rewards');
-		$this->treatments = CustomTable::for('referral_treatments');
-
 		add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
 	}
 
@@ -235,33 +227,17 @@
 			return $this->error('referral_id required', 'missing_id', 400);
 		}
 
-		// Get referral using CustomTable
-		$referral = $this->referrals->get(['id' => $referral_id]);
-		if (!$referral) {
-			return $this->notFound('Referral not found');
+		$result = JVB()->referrals()->updateStatus($referral_id, $status);
+
+		if (is_wp_error($result)) {
+			$code = $result->get_error_code();
+			return $code === 'not_found'
+				? $this->notFound($result->get_error_message())
+				: $this->error($result->get_error_message(), $code, 500);
 		}
 
-		// Update status
-		$update_data = ['status' => $status];
-		$update_data["{$status}_at"] = current_time('mysql');
-
-		if ($status === 'treated') {
-			$update_data['treatment_count'] = ($referral->treatment_count ?? 0) + 1;
-		}
-
-		$updated = $this->referrals->update($update_data, ['id' => $referral_id]);
-
-		if ($updated !== false) {
-			// Create rewards if treated
-			if ($status === 'treated') {
-				$this->createRewards($referral);
-			}
-
-			$this->cache->flush();
-			return $this->success(['message' => "Referral marked as {$status}"]);
-		}
-
-		return $this->error('Failed to update referral', 'update_failed', 500);
+		$this->cache->flush();
+		return $this->success(['message' => "Referral marked as {$status}"]);
 	}
 
 	/**
@@ -274,26 +250,20 @@
 			return $this->error('referral_id required', 'missing_id', 400);
 		}
 
-		// Get referral
-		$referral = $this->referrals->get(['id' => $referral_id]);
-		if (!$referral) {
-			return $this->notFound('Referral not found');
+		$result = JVB()->referrals()->removeReferral($referral_id, get_current_user_id());
+
+		if (is_wp_error($result)) {
+			$code = $result->get_error_code();
+			$status = match($code) {
+				'not_found'      => 404,
+				'unauthorized'   => 403,
+				'invalid_status' => 400,
+				default          => 500
+			};
+			return $this->error($result->get_error_message(), $code, $status);
 		}
 
-		// Check ownership
-		$current_user_id = get_current_user_id();
-		if ($referral->referrer_id != $current_user_id && !current_user_can('manage_options')) {
-			return $this->forbidden('Unauthorized');
-		}
-
-		// Can only remove pending referrals
-		if ($referral->status !== 'pending') {
-			return $this->error('Can only remove pending referrals', 'invalid_status', 400);
-		}
-
-		$this->referrals->delete(['id' => $referral_id]);
 		$this->cache->flush();
-
 		return $this->success(['message' => 'Referral removed']);
 	}
 
@@ -307,44 +277,18 @@
 			return $this->error('referral_id required', 'missing_id', 400);
 		}
 
-		$current_user_id = get_current_user_id();
-
-		// Get referral with ownership check
-		$referral = $this->referrals->where([
-			'id' => $referral_id,
-			'referrer_id' => $current_user_id
-		])->first();
-
-		if (!$referral) {
-			return $this->notFound('Referral not found');
-		}
-
-		// Check rate limit (once per week)
-		$transient_key = 'referral_last_invite_' . md5($referral->referee_email);
-		$last_invite = get_transient($transient_key);
-
-		if ($last_invite && (time() - $last_invite) < WEEK_IN_SECONDS) {
-			return $this->error(
-				'Can only resend once per week',
-				'rate_limit',
-				429
-			);
-		}
-
-		// Resend via referral manager
-		$result = JVB()->referrals()->sendReferralInvitation(
-			$current_user_id,
-			$referral->referee_email,
-			$referral->referee_name,
-			sprintf('Reminder: Join %s', get_bloginfo('name')),
-			'Just a friendly reminder about my invitation!'
-		);
+		$result = JVB()->referrals()->resendInvitation($referral_id, get_current_user_id());
 
 		if (is_wp_error($result)) {
-			return $this->error($result->get_error_message(), 'send_failed', 500);
+			$code = $result->get_error_code();
+			$status = match($code) {
+				'not_found'  => 404,
+				'rate_limit' => 429,
+				default      => 500
+			};
+			return $this->error($result->get_error_message(), $code, $status);
 		}
 
-		set_transient($transient_key, time(), WEEK_IN_SECONDS);
 		return $this->success(['message' => 'Invitation resent']);
 	}
 
@@ -461,90 +405,19 @@
 	 */
 	protected function getAllReferrals(WP_REST_Request $request): WP_REST_Response
 	{
-		global $wpdb;
-
-		$where = ['1=1'];
-		$where_params = [];
-
-		// Build WHERE conditions
-		$status = $request->get_param('status');
-		if ($status && $status !== 'all') {
-			$where[] = 'status = %s';
-			$where_params[] = $status;
-		}
-
-		if ($date_start = $request->get_param('date_start')) {
-			$where[] = 'referred_at >= %s';
-			$where_params[] = $date_start;
-		}
-
-		if ($date_end = $request->get_param('date_end')) {
-			$where[] = 'referred_at <= %s';
-			$where_params[] = $date_end;
-		}
-
-		$search = $request->get_param('search');
-		if (!empty($search)) {
-			$search_term = '%' . $wpdb->esc_like($search) . '%';
-			$where[] = '(r.referee_name LIKE %s OR r.referee_email LIKE %s OR r.referral_code LIKE %s OR u.display_name LIKE %s)';
-			$where_params[] = $search_term;
-			$where_params[] = $search_term;
-			$where_params[] = $search_term;
-			$where_params[] = $search_term;
-		}
-
-		$limit = $request->get_param('limit') ?? 50;
-		$offset = $request->get_param('offset') ?? 0;
-
-		$where_params[] = $limit;
-		$where_params[] = $offset;
-
-		// Use CustomTable's query method
-		$query = "SELECT r.*, u.display_name as referrer_name
-			FROM {table} r
-			LEFT JOIN {$wpdb->users} u ON r.referrer_id = u.ID
-			WHERE " . implode(' AND ', $where) . "
-			ORDER BY referred_at DESC
-			LIMIT %d OFFSET %d";
-
-		$items = $this->referrals->queryResults($query, $where_params);
-
-		return $this->success([
-			'items' => $items,
-			'total' => count($items)
-		]);
-	}
-
-	/**
-	 * Helper: Create rewards for completed referral
-	 */
-	protected function createRewards(object $referral): void
-	{
-		$settings = JVB()->referrals()->getRewardSettings();
-
-		// Referrer reward
-		$this->rewards->insert([
-			'referral_id' => $referral->id,
-			'user_id' => $referral->referrer_id,
-			'reward_type' => 'referrer',
-			'amount' => $settings['referrer_reward_amount'],
-			'reward_calculation' => $settings['referrer_reward_type'],
-			'status' => 'available'
+		$items = JVB()->referrals()->getAllReferrals([
+			'status'     => $request->get_param('status') ?? 'all',
+			'search'     => $request->get_param('search'),
+			'date_start' => $request->get_param('date_start'),
+			'date_end'   => $request->get_param('date_end'),
+			'limit'      => $request->get_param('limit') ?? 50,
+			'offset'     => $request->get_param('offset') ?? 0,
 		]);
 
-		// Referee reward
-		if ($referral->referee_id) {
-			$this->rewards->insert([
-				'referral_id' => $referral->id,
-				'user_id' => $referral->referee_id,
-				'reward_type' => 'referee',
-				'amount' => $settings['referee_reward_amount'],
-				'reward_calculation' => $settings['referee_reward_type'],
-				'status' => 'available'
-			]);
-		}
+		return $this->success(['items' => $items, 'total' => count($items)]);
 	}
 
+
 	/**
 	 * Process queued referral operations
 	 */
diff --git a/jvb.php b/jvb.php
index d5da153..4e33269 100644
--- a/jvb.php
+++ b/jvb.php
@@ -20,6 +20,7 @@
     exit;
 }
 
+
 /**
  * Track REST API errors by wrapping request execution
  */
@@ -574,3 +575,10 @@
 		jvbIcon('caret-double-up')
 	);
 }
+
+add_action( 'doing_it_wrong_run', function ( $function_name ) {
+	if ( '_load_textdomain_just_in_time' === $function_name ) {
+		// This will print the full execution path to your screen or log
+		debug_print_backtrace();
+	}
+} );
diff --git a/src/drawer-menu/style.scss b/src/drawer-menu/style.scss
index 6c80a9d..30dc312 100644
--- a/src/drawer-menu/style.scss
+++ b/src/drawer-menu/style.scss
@@ -33,6 +33,7 @@
 
 	.toggle {
 		width: 100%;
+		aspect-ratio: unset;
 		.icon {
 			transform: rotate(0);
 			transition: var(--trans-transform);
@@ -63,6 +64,7 @@
 		margin: 0;
 		height: auto;
 		width:100%;
+		overflow: hidden;
 	}
 	li {
 		height: auto;
@@ -86,3 +88,7 @@
 	}
 
 }
+
+.main-actions .buttons.buttons {
+	right: calc(var(--btn_) + .5rem);
+}
diff --git a/src/gmbreviews/render.php b/src/gmbreviews/render.php
index 487f394..779c1c4 100644
--- a/src/gmbreviews/render.php
+++ b/src/gmbreviews/render.php
@@ -57,7 +57,7 @@
 		ob_start();
 		?>
 		<div class="gmb-reviews">
-			<div class="row center">
+			<div class="row x-mid">
 			<?php
 			if ($showStats && !empty($average) && !empty($total)) {
 				?>
@@ -97,7 +97,7 @@
 			if ($showReviewLink && !empty($reviewUrl)) {
 				?>
 				<a href="<?=esc_url($reviewUrl)?>"
-				   class="button"
+				   class="btn"
 				   target="_blank"
 				   rel="noopener noreferrer">
 					<?= jvbIcon('star', ['style' => 'fill']) ?>
@@ -128,6 +128,7 @@
 				<li>
 					<blockquote class="review">
 						<?php
+						echo jvbIcon('quotes',['style' => 'fill']);
 						// Review text
 						if (!empty($comment)) { ?>
 							<div class="content review">
@@ -179,7 +180,7 @@
 			?>
 			<div class="footer">
 				<a href=" <?= esc_url($viewAllUrl) ?>"
-					class="button"
+					class="btn"
 					target="_blank"
 					rel="noopener noreferrer">
 
diff --git a/src/gmbreviews/style.scss b/src/gmbreviews/style.scss
index bb0e08d..721a94c 100644
--- a/src/gmbreviews/style.scss
+++ b/src/gmbreviews/style.scss
@@ -1,6 +1,6 @@
 .gmb-reviews {
 	max-width: none;
-	> .row.center {
+	> .row.x-mid {
 		max-width:var(--content);
 		margin: 0 auto;
 		--gap: .5rem 6rem;
@@ -45,9 +45,14 @@
 		}
 	}
 	blockquote {
-		margin:0 auto;
-		padding: 0;
+		margin: 1rem auto;
+		padding: 1rem 0 0;
 		max-width: var(--content);
+		border-top: 1px solid rgb(var(--base-200));
+
+		.icon-quotes-fi {
+			left: calc(var(--btnbtn) * -1);
+		}
 		.content {
 			border-width: 4px 1px;
 			&::after {
@@ -60,6 +65,7 @@
 		}
 		cite {
 			position: relative;
+			margin-top: var(--btn);
 
 			img {
 				width: 4.5rem;
@@ -116,7 +122,7 @@
 			color: rgb(var(--action-0));
 		}
 	}
-	.footer .button {
+	.btn {
 		width: 100%;
 	}
 }

--
Gitblit v1.10.0