Plugin Directory

Changeset 3454888


Ignore:
Timestamp:
02/05/2026 06:51:20 PM (2 weeks ago)
Author:
psakhilsoman
Message:

Release 1.2

Location:
faecursor
Files:
598 added
5 edited

Legend:

Unmodified
Added
Removed
  • faecursor/trunk/README.txt

    r3384653 r3454888  
    1 === FaeCursor | WordPress Custom Cursor Plugin ===
    2 Contributors: psakhilsoman
     1=== FaeCursor | WordPress Custom Cursor, Keyboard & Screen Effects ===
     2Contributors: psakhilsoman, faecursor
    33Author: FaeCursor Plugin Team
    4 Version: 1.1
    5 Tags: cursor effects, animation, custom icons, user interaction, visual effects
     4Version: 1.2
     5Tags: wordpress custom cursor, custom cursor wordpress plugin, mouse cursor effects, keyboard effects, screen effects, particle effects, interaction effects, animation effects, UI animation
    66Requires at least: 5.6
    7 Tested up to: 6.8.3
    8 Stable tag: 1.1
     7Tested up to: 6.9.1
     8Stable tag: 1.2
    99Requires PHP: 7.4
    1010License: GPLv2 or later
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1212
    13 FaeCursor adds unique mouse cursor effects.
     13Bring your WordPress site to life with lightweight **custom cursor, keyboard, and screen effects** — star trails, sparkles, particle animations, and smooth interactive UI effects, fully optimized for performance.
    1414
    1515== Description ==
    1616
    17 FaeCursor is a highly customizable WordPress plugin that allows you to add beautiful mouse cursor effects to your website. With a variety of effects to choose from, you can create engaging user experiences that leave a lasting impression. Whether you want sparkles, stars etc, FaeCursor has you covered.
     17FaeCursor is a lightweight **WordPress custom cursor plugin** that adds interactive cursor, keyboard, and screen effects to your website. Create engaging experiences with smooth, responsive animations such as **cursor trails, sparkles, stars, and subtle screen particles**. Perfect for Elementor, Divi, and any WordPress theme, FaeCursor enhances user engagement without slowing your pages.
    1818
    1919**Features:**
    20 - Multiple cursor effects, including star trails, sparkles, and magic aura.
    21 - Easy-to-use settings page with real-time effect previews.
    22 - Fully responsive and optimized for performance.
    23 - Works with any WordPress theme.
     20- Multiple **cursor effects**, including star trails, sparkles, dual circles, and magic aura.
     21- **Keyboard interaction effects** and shortcut animations for dynamic UI engagement.
     22- **Screen effects and particle animations** triggered by user actions.
     23- Easy-to-use admin panel with **real-time preview** of all effects.
     24- Fully responsive, lightweight, and optimized for **performance**.
     25- Works seamlessly with any WordPress theme, Elementor, and Divi.
    2426
    2527**Why Choose FaeCursor?**
    26 - Add an extra layer of engagement to your site with stunning visual effects.
    27 - Easily manage and update effects from an intuitive plugin settings screen.
     28- Boost user engagement with interactive **cursor, keyboard, and screen effects**.
     29- Customize animations easily through an intuitive plugin settings screen.
     30- Lightweight and performance-optimized — won’t slow your site.
    2831
    2932== Installation ==
    3033
    31 1. Upload the `FaeCursor` folder to the `/wp-content/plugins/` directory.
     341. Upload the `faecursor` folder to the `/wp-content/plugins/` directory.
    32352. Activate the plugin through the 'Plugins' screen in WordPress.
    33 3. Go to the FaeCursor settings page in your WordPress dashboard to configure the effects.
     363. Go to the FaeCursor settings page in your WordPress dashboard to configure effects.
    3437
    3538== Frequently Asked Questions ==
    3639
    3740= Does FaeCursor work with all themes? =
    38 Absolutely! FaeCursor is designed to work with any WordPress theme without interfering with your existing design.
     41Yes! FaeCursor is fully compatible with any WordPress theme, including Elementor and Divi, without affecting page layout.
    3942
    4043= Will FaeCursor slow down my site? =
    41 No, FaeCursor is optimized for performance. We’ve ensured that the plugin is lightweight and won’t negatively impact your website speed.
     44No, the plugin is lightweight and optimized for performance. Smooth animations will not impact your site speed.
    4245
    4346== Screenshots ==
     
    45481. **Plugin Settings Screen** screenshot-1.png
    46492. **Sparkle Effect** screenshot-2.png
     503. **Keyboard Interaction Effects** screenshot-3.png
     514. **Screen Particle Effects** screenshot-4.png
    4752
    4853== Changelog ==
     54
     55= 05 Feb 2026 - ver 1.2.0 =
     56* Updated admin UI with enhanced usability and live preview.
     57* Added new cursor, keyboard, and screen effects.
     58* Optimized performance for modern browsers and WordPress versions.
    4959
    5060= 25 Oct 2025 - ver 1.1.0 =
     
    5969== Upgrade Notice ==
    6070
     71= 1.2.0 =
     72This update adds keyboard and screen interaction effects with a fully improved admin UI for live previews.
     73
    6174= 1.1.0 =
    62 This update brings a refreshed admin UI, new icons for enhanced customization, and a fix for Safari users experiencing issues with the dual circle effect. Make sure to update to enjoy the improved experience!
     75Refreshed admin UI, new icons, and Safari dual circle fix. Update for the improved experience.
    6376
    6477= 1.0.0 =
    65 Initial release. Enjoy the new visual effects!
     78Initial release. Enjoy interactive cursor effects on your WordPress site!
    6679
    6780== License ==
  • faecursor/trunk/assets/css/fae-cursor-admin.css

    r3384653 r3454888  
    11/* FaeCursor Admin Dashboard Styles */
     2
     3/* Hide or relocate WordPress admin notices on FaeCursor page */
     4.toplevel_page_fae_cursor .notice,
     5.toplevel_page_fae_cursor .update-nag,
     6.toplevel_page_fae_cursor .error,
     7.toplevel_page_fae_cursor .updated,
     8.toplevel_page_fae_cursor .is-dismissible {
     9  margin: 20px 0 20px 0 !important;
     10  position: relative;
     11  z-index: 1;
     12}
     13
     14/* Move notices below the header */
     15.toplevel_page_fae_cursor .wrap {
     16  position: relative;
     17}
     18
     19.toplevel_page_fae_cursor .fae-cursor-dashboard {
     20  position: relative;
     21  z-index: 2;
     22}
     23
     24/* Ensure notices don't appear above header */
     25.toplevel_page_fae_cursor .notice:not(.fae-notice) {
     26  margin-top: 0 !important;
     27  margin-bottom: 20px !important;
     28}
    229
    330/* Admin Menu Icon */
     
    1340.fae-cursor-dashboard {
    1441  max-width: 1200px;
    15   margin: 20px 0;
     42  margin: 20px auto;
    1643  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    1744}
     
    2754}
    2855
    29 .fae-dashboard-header img {
    30   object-fit: contain;
    31   opacity: 1 !important;
    32   height: 50px;
    33   padding: 0 !important;
    34 }
    35 
    3656.fae-dashboard-title {
    3757  color: rgb(255, 255, 255);
     
    4363  align-items: center;
    4464  gap: 20px;
     65  position: relative;
     66  z-index: 2;
    4567}
    4668
     
    4971}
    5072
     73.fae-header-actions {
     74  display: flex;
     75  align-items: center;
     76}
     77
    5178.fae-dashboard-header h1 {
    52   margin: 0 0 10px 0;
     79  margin: 0;
    5380  font-size: 2.5em;
    5481  font-weight: 700;
    5582  display: flex;
    5683  align-items: center;
     84  gap: 12px;
    5785}
    5886
     
    6391}
    6492
    65 .fae-dashboard-header p {
    66   margin: 0;
    67   font-size: 1.1em;
    68   opacity: 0.9;
    69 }
    70 
    71 .fae-header-actions {
    72   display: flex;
    73   align-items: center;
    74 }
    75 
    76 .fae-btn-feedback {
     93.fae-title-group {
     94  display: flex;
     95  flex-direction: column;
     96  gap: 2px;
     97}
     98
     99.fae-title-name {
     100  display: flex;
     101  align-items: center;
     102  gap: 10px;
     103  position: relative;
     104}
     105
     106.fae-tagline {
     107  font-size: 0.30em;
     108  font-weight: 400;
     109  opacity: 0.85;
     110  letter-spacing: 1px;
     111}
     112
     113.fae-version-badge {
     114  font-size: 0.45em;
     115  font-weight: 600;
    77116  background: rgba(255, 255, 255, 0.2);
    78   color: white;
    79   border: 2px solid rgba(255, 255, 255, 0.3);
    80   padding: 12px 20px;
    81   border-radius: 8px;
    82   text-decoration: none;
    83   font-weight: 600;
    84   display: flex;
    85   align-items: center;
    86   gap: 8px;
    87   transition: all 0.3s ease;
    88   backdrop-filter: blur(10px);
    89 }
    90 
    91 .fae-btn-feedback:hover {
    92   background: rgba(255, 255, 255, 0.3);
    93   border-color: rgba(255, 255, 255, 0.5);
    94   color: white;
    95   transform: translateY(-2px);
    96   box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
    97 }
    98 
    99 .fae-btn-feedback .fae-icon {
    100   width: 16px;
    101   height: 16px;
    102   fill: currentColor;
     117  padding: 3px 8px;
     118  border-radius: 20px;
     119  vertical-align: middle;
     120  letter-spacing: 0.5px;
    103121}
    104122
     
    106124.fae-stats-grid {
    107125  display: grid;
    108   grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
     126  grid-template-columns: repeat(3, 1fr);
    109127  gap: 20px;
    110128  margin-bottom: 30px;
     129}
     130
     131@media (max-width: 900px) {
     132  .fae-stats-grid {
     133    grid-template-columns: repeat(2, 1fr);
     134  }
     135}
     136
     137@media (max-width: 600px) {
     138  .fae-stats-grid {
     139    grid-template-columns: 1fr;
     140  }
    111141}
    112142
     
    140170}
    141171
     172/* Inactive state - dull/muted styling */
     173.fae-stat-card-inactive {
     174  border-left-color: #9ca3af;
     175  opacity: 0.75;
     176}
     177
     178.fae-stat-card-inactive .fae-stat-value {
     179  color: #9ca3af;
     180}
     181
     182.fae-stat-card-inactive:hover {
     183  transform: translateY(-3px);
     184  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
     185}
     186
     187/* Multiple effects - smaller text */
     188.fae-stat-value-small {
     189  font-size: 2em;
     190}
     191
     192/* Detail text for multiple effects */
     193.fae-stat-detail {
     194  margin: 8px 0 0 0;
     195  font-size: 0.85em;
     196  color: #6b7280;
     197  font-weight: 500;
     198  line-height: 1.4;
     199}
     200
    142201/* Main Content */
    143202.fae-main-content {
     
    153212}
    154213
    155 .fae-settings-panel h2 {
    156   margin: 0 0 25px 0;
    157   color: #333;
    158   font-size: 1.5em;
    159   display: flex;
    160   align-items: center;
    161   gap: 10px;
    162 }
    163214
    164215.fae-settings-panel .fae-icon {
     
    171222.fae-effect-grid {
    172223  display: grid;
    173   grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    174   gap: 15px;
     224  grid-template-columns: repeat(auto-fill, minmax(140px, 170px));
     225  gap: 12px;
    175226  margin-bottom: 25px;
    176227}
     
    181232  border: 2px solid #e1e5e9;
    182233  border-radius: 8px;
    183   padding: 20px;
     234  padding: 12px 10px;
    184235  text-align: center;
    185236  transition: all 0.3s ease;
    186237  background: white;
     238  height: 75px;
     239  display: flex;
     240  align-items: center;
     241  justify-content: center;
    187242}
    188243
     
    198253  pointer-events: none;
    199254}
    200 
    201 .fae-effect-option input[type="radio"]:checked + .fae-effect-content {
     255/* Visual "selected" state for effect card (minimal styling) */
     256.fae-effect-option.fae-effect-selected {
     257  border-color: #667eea;
     258  background: #ffffff;
    202259  color: #667eea;
    203260}
    204261
    205 .fae-effect-option input[type="radio"]:checked {
    206   border-color: #667eea;
    207   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    208   color: white;
    209 }
    210 
    211262.fae-effect-content {
    212263  display: flex;
    213264  flex-direction: column;
    214265  align-items: center;
     266  justify-content: center;
    215267  gap: 10px;
     268  position: relative;
     269  width: 100%;
    216270}
    217271
     
    227281}
    228282
     283/* Letter Avatar - Clean Subtle Design */
     284.fae-effect-letter {
     285  width: 36px;
     286  height: 36px;
     287  border-radius: 8px;
     288  background: #f1f5f9;
     289  color: #64748b;
     290  display: flex;
     291  align-items: center;
     292  justify-content: center;
     293  font-size: 12px;
     294  font-weight: 600;
     295  letter-spacing: 0.5px;
     296  text-transform: uppercase;
     297  flex-shrink: 0;
     298  transition: all 0.2s ease;
     299  border: 1px solid #e2e8f0;
     300}
     301
     302.fae-effect-option:hover .fae-effect-letter {
     303  background: #e2e8f0;
     304  color: #475569;
     305  border-color: #cbd5e1;
     306}
     307/* Letter style when card is selected */
     308.fae-effect-option.fae-effect-selected .fae-effect-letter {
     309  background: #667eea;
     310  color: #ffffff;
     311  border-color: #667eea;
     312}
     313
     314/* Active Effect Badge */
     315.fae-effect-active-badge {
     316  position: absolute;
     317  top: 6px;
     318  right: 6px;
     319  background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
     320  color: white;
     321  font-size: 9px;
     322  font-weight: 700;
     323  padding: 3px 6px;
     324  border-radius: 4px;
     325  text-transform: uppercase;
     326  letter-spacing: 0.5px;
     327  box-shadow: 0 2px 6px rgba(76, 175, 80, 0.3);
     328  z-index: 10;
     329  pointer-events: none;
     330  line-height: 1.2;
     331}
     332
     333.fae-effect-active {
     334  border-color: #4caf50 !important;
     335  box-shadow: 0 0 0 1px rgba(76, 175, 80, 0.2);
     336}
     337
     338.fae-effect-active:hover {
     339  border-color: #4caf50 !important;
     340  box-shadow: 0 5px 15px rgba(76, 175, 80, 0.25);
     341}
     342
    229343/* Effect Settings */
    230344.fae-effect-settings {
    231345  margin-top: 25px;
    232346}
     347
     348/* ═══════════════════════════════════════════════════════════════ */
     349/* NEW GROUPED SETTINGS LAYOUT */
     350/* ═══════════════════════════════════════════════════════════════ */
     351
     352.fae-settings-group-container {
     353  background: #ffffff;
     354  border-radius: 16px;
     355  margin-bottom: 24px;
     356  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
     357  border: 1px solid #e8eaed;
     358  overflow: hidden;
     359}
     360
     361.fae-settings-group-header {
     362  background: linear-gradient(135deg, #f8f9ff 0%, #f3f4f8 100%);
     363  padding: 18px 24px;
     364  border-bottom: 1px solid #e8eaed;
     365  display: flex;
     366  align-items: center;
     367}
     368
     369.fae-settings-group-text {
     370  flex: 1;
     371}
     372
     373.fae-settings-group-title {
     374  margin: 0 0 2px 0;
     375  font-size: 1.1em;
     376  font-weight: 700;
     377  color: #1a1a2e;
     378}
     379
     380.fae-settings-group-description {
     381  margin: 0;
     382  font-size: 0.85em;
     383  color: #6b7280;
     384}
     385
     386.fae-settings-group-content {
     387  padding: 24px;
     388}
     389
     390/* Settings Row - Horizontal layout for related fields */
     391.fae-settings-row {
     392  display: grid;
     393  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
     394  gap: 20px;
     395  margin-bottom: 0;
     396}
     397
     398.fae-setting-item {
     399  display: flex;
     400  flex-direction: column;
     401  gap: 8px;
     402}
     403
     404.fae-setting-item-wide {
     405  grid-column: span 2;
     406}
     407
     408.fae-setting-label {
     409  font-size: 14px;
     410  font-weight: 600;
     411  color: #374151;
     412}
     413
     414.fae-setting-hint {
     415  font-size: 12px;
     416  color: #6b7280;
     417  margin: 4px 0 0 0;
     418}
     419
     420/* Settings Divider */
     421.fae-settings-divider {
     422  display: flex;
     423  align-items: center;
     424  margin: 24px 0 20px 0;
     425  gap: 12px;
     426}
     427
     428.fae-settings-divider::before,
     429.fae-settings-divider::after {
     430  content: '';
     431  flex: 1;
     432  height: 1px;
     433  background: #e5e7eb;
     434}
     435
     436.fae-settings-divider span {
     437  font-size: 12px;
     438  font-weight: 600;
     439  color: #9ca3af;
     440  text-transform: uppercase;
     441  letter-spacing: 0.5px;
     442}
     443
     444/* Subgroups within a group */
     445.fae-settings-subgroup {
     446  background: #f9fafb;
     447  border-radius: 12px;
     448  padding: 20px;
     449  margin-bottom: 16px;
     450  border: 1px solid #f0f1f3;
     451}
     452
     453.fae-settings-subgroup:last-child {
     454  margin-bottom: 0;
     455}
     456
     457.fae-settings-subgroup-header {
     458  display: flex;
     459  align-items: center;
     460  gap: 10px;
     461  margin-bottom: 16px;
     462  padding-bottom: 12px;
     463  border-bottom: 1px solid #e5e7eb;
     464}
     465
     466.fae-subgroup-icon {
     467  width: 20px;
     468  height: 20px;
     469  fill: #667eea;
     470  stroke: #667eea;
     471  flex-shrink: 0;
     472}
     473
     474.fae-subgroup-title {
     475  font-size: 14px;
     476  font-weight: 600;
     477  color: #374151;
     478}
     479
     480.fae-settings-subgroup-content {
     481  padding: 16px 0;
     482}
     483
     484/* Device Toggles */
     485.fae-device-toggles {
     486  display: flex;
     487  flex-wrap: wrap;
     488  gap: 16px;
     489}
     490
     491.fae-device-toggles .fae-toggle-switch {
     492  display: flex;
     493  align-items: center;
     494  gap: 10px;
     495  cursor: pointer;
     496  padding: 8px 16px;
     497  background: white;
     498  border-radius: 8px;
     499  border: 1px solid #e5e7eb;
     500  transition: all 0.2s ease;
     501}
     502
     503.fae-device-toggles .fae-toggle-switch:hover {
     504  border-color: #667eea;
     505  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
     506}
     507
     508.fae-device-toggles .fae-toggle-label {
     509  font-size: 13px;
     510  font-weight: 500;
     511  color: #374151;
     512}
     513
     514/* Radio Group */
     515.fae-radio-group {
     516  display: flex;
     517  flex-direction: column;
     518  gap: 12px;
     519  margin-bottom: 16px;
     520}
     521
     522.fae-radio-item-disabled {
     523  opacity: 0.6;
     524  cursor: not-allowed;
     525  pointer-events: none;
     526}
     527
     528.fae-radio-item-disabled .fae-radio-label {
     529  color: #9ca3af;
     530}
     531
     532.fae-radio-item-disabled input[type="radio"] {
     533  cursor: not-allowed;
     534}
     535
     536.fae-radio-item {
     537  display: flex;
     538  align-items: flex-start;
     539  gap: 12px;
     540  padding: 12px 16px;
     541  background: white;
     542  border-radius: 8px;
     543  border: 2px solid #e5e7eb;
     544  cursor: pointer;
     545  transition: all 0.2s ease;
     546}
     547
     548.fae-radio-item:hover {
     549  border-color: #c7d2fe;
     550  background: #fafaff;
     551}
     552
     553.fae-radio-item:has(input:checked) {
     554  border-color: #667eea;
     555  background: #f5f3ff;
     556}
     557
     558.fae-radio-item input[type="radio"] {
     559  margin-top: 2px;
     560  accent-color: #667eea;
     561}
     562
     563.fae-radio-label {
     564  display: flex;
     565  flex-direction: column;
     566  gap: 2px;
     567}
     568
     569.fae-radio-label strong {
     570  font-size: 14px;
     571  color: #1f2937;
     572}
     573
     574.fae-radio-label small {
     575  font-size: 12px;
     576  color: #6b7280;
     577}
     578
     579/* Checkbox List */
     580.fae-checkbox-list {
     581  max-height: 200px;
     582  overflow-y: auto;
     583  background: white;
     584  border: 1px solid #e5e7eb;
     585  border-radius: 8px;
     586  padding: 12px;
     587}
     588
     589.fae-checkbox-item {
     590  display: flex;
     591  align-items: center;
     592  gap: 8px;
     593  padding: 8px 12px;
     594  cursor: pointer;
     595  border-radius: 6px;
     596  transition: background 0.15s ease;
     597}
     598
     599.fae-checkbox-item:hover {
     600  background: #f3f4f6;
     601}
     602
     603.fae-checkbox-item input[type="checkbox"] {
     604  accent-color: #667eea;
     605}
     606
     607.fae-checkbox-item span {
     608  font-size: 14px;
     609  color: #374151;
     610}
     611
     612/* Roles Checkbox Grid */
     613.fae-roles-checkbox-grid {
     614  display: grid;
     615  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
     616  gap: 8px;
     617  margin-bottom: 16px;
     618}
     619
     620.fae-checkbox-highlighted {
     621  background: #fef3c7;
     622  border: 1px solid #fcd34d;
     623  border-radius: 8px;
     624  margin-top: 12px;
     625  padding-top: 12px;
     626}
     627
     628.fae-checkbox-highlighted:hover {
     629  background: #fef3c7;
     630}
     631
     632/* Specific Roles Wrapper */
     633.fae-specific-roles-wrapper {
     634  background: #f9fafb;
     635  border-radius: 8px;
     636  padding: 16px;
     637  margin-top: 12px;
     638  border: 1px solid #e5e7eb;
     639}
     640
     641/* Scope Pages Wrapper - matches roles wrapper styling */
     642.fae-scope-pages-wrapper {
     643  background: #f9fafb;
     644  border-radius: 8px;
     645  padding: 16px;
     646  margin-top: 12px;
     647  border: 1px solid #e5e7eb;
     648}
     649
     650/* Pages Checkbox Grid - matches roles grid */
     651.fae-pages-checkbox-grid {
     652  display: grid;
     653  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
     654  gap: 8px;
     655  max-height: 250px;
     656  overflow-y: auto;
     657  padding-right: 8px;
     658}
     659
     660/* Scrollbar for pages grid */
     661.fae-pages-checkbox-grid::-webkit-scrollbar {
     662  width: 6px;
     663}
     664
     665.fae-pages-checkbox-grid::-webkit-scrollbar-track {
     666  background: #f1f1f1;
     667  border-radius: 3px;
     668}
     669
     670.fae-pages-checkbox-grid::-webkit-scrollbar-thumb {
     671  background: #c1c1c1;
     672  border-radius: 3px;
     673}
     674
     675.fae-pages-checkbox-grid::-webkit-scrollbar-thumb:hover {
     676  background: #a8a8a8;
     677}
     678
     679/* CSS Selector Wrapper */
     680.fae-scope-selector-wrapper {
     681  background: #f9fafb;
     682  border-radius: 8px;
     683  padding: 16px;
     684  margin-top: 12px;
     685  border: 1px solid #e5e7eb;
     686}
     687
     688.fae-scope-selector-wrapper input[type="text"] {
     689  width: 100%;
     690  padding: 12px 16px;
     691  border: 2px solid #e5e7eb;
     692  border-radius: 8px;
     693  font-size: 14px;
     694  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
     695  background: white;
     696  transition: all 0.2s ease;
     697}
     698
     699.fae-scope-selector-wrapper input[type="text"]:focus {
     700  outline: none;
     701  border-color: #667eea;
     702  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
     703}
     704
     705.fae-scope-selector-wrapper input[type="text"]:hover {
     706  border-color: #c7d2fe;
     707}
     708
     709/* Icon Settings Wrapper */
     710.fae-icon-settings-wrapper {
     711  margin-top: 0;
     712}
     713
     714/* ═══════════════════════════════════════════════════════════════ */
     715/* LEGACY SUPPORT - Old section styles (for other tabs) */
     716/* ═══════════════════════════════════════════════════════════════ */
    233717
    234718.fae-settings-section {
     
    285769}
    286770
     771/* Responsive for grouped settings */
     772@media (max-width: 768px) {
     773  .fae-settings-group-header {
     774    flex-direction: column;
     775    align-items: flex-start;
     776    text-align: left;
     777  }
     778 
     779  .fae-settings-row {
     780    grid-template-columns: 1fr;
     781  }
     782 
     783  .fae-setting-item-wide {
     784    grid-column: span 1;
     785  }
     786 
     787  .fae-device-toggles {
     788    flex-direction: column;
     789  }
     790 
     791  .fae-device-toggles .fae-toggle-switch {
     792    width: 100%;
     793  }
     794 
     795  .fae-roles-checkbox-grid {
     796    grid-template-columns: 1fr;
     797  }
     798}
     799
    287800.fae-info-card {
    288801  background: white;
     
    347860  border-color: #667eea;
    348861  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
     862}
     863
     864/* New grouped layout input styles */
     865.fae-setting-item input[type="text"],
     866.fae-setting-item input[type="number"],
     867.fae-setting-item select {
     868  width: 100%;
     869  padding: 10px 15px;
     870  border: 2px solid #e1e5e9;
     871  border-radius: 8px;
     872  font-size: 14px;
     873  transition: all 0.2s ease;
     874  background: white;
     875}
     876
     877.fae-setting-item input[type="text"]:focus,
     878.fae-setting-item input[type="number"]:focus,
     879.fae-setting-item select:focus {
     880  outline: none;
     881  border-color: #667eea;
     882  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
     883}
     884
     885.fae-setting-item input[type="text"]:hover,
     886.fae-setting-item input[type="number"]:hover,
     887.fae-setting-item select:hover {
     888  border-color: #c7d2fe;
    349889}
    350890
     
    6621202}
    6631203
     1204.fae-btn-danger {
     1205  background: #dc3545;
     1206  color: white;
     1207}
     1208
     1209.fae-btn-danger:hover {
     1210  background: #c82333;
     1211  transform: translateY(-2px);
     1212  box-shadow: 0 5px 15px rgba(220, 53, 69, 0.4);
     1213}
     1214
    6641215.fae-btn-outline {
    6651216  background: transparent;
     
    7181269    flex-direction: column;
    7191270    gap: 15px;
    720   }
    721 
    722   .fae-header-actions {
    723     align-self: flex-start;
    7241271  }
    7251272
     
    7571304  }
    7581305}
     1306
     1307
     1308/* Info Button */
     1309.fae-info-button {
     1310  position: relative;
     1311  background: none;
     1312  border: none;
     1313  padding: 4px;
     1314  cursor: pointer;
     1315  display: inline-flex;
     1316  align-items: center;
     1317  justify-content: center;
     1318  margin-left: auto;
     1319  color: #667eea;
     1320  transition: all 0.3s ease;
     1321  border-radius: 50%;
     1322  width: 24px;
     1323  height: 24px;
     1324  flex-shrink: 0;
     1325}
     1326
     1327.fae-info-button:hover {
     1328  background: rgba(102, 126, 234, 0.1);
     1329  color: #764ba2;
     1330}
     1331
     1332.fae-info-button .fae-icon {
     1333  width: 18px;
     1334  height: 18px;
     1335  fill: currentColor;
     1336}
     1337
     1338/* Inline Info Button (for settings) */
     1339.fae-info-button-inline {
     1340  margin-left: 4px;
     1341  width: 18px;
     1342  height: 18px;
     1343  padding: 2px;
     1344}
     1345
     1346.fae-info-button-inline .fae-icon {
     1347  width: 14px;
     1348  height: 14px;
     1349}
     1350
     1351/* Tooltip */
     1352.fae-tooltip {
     1353  position: absolute;
     1354  bottom: calc(100% + 10px);
     1355  right: 0;
     1356  background: white;
     1357  border-radius: 8px;
     1358  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
     1359  padding: 0;
     1360  min-width: 280px;
     1361  max-width: 320px;
     1362  opacity: 0;
     1363  visibility: hidden;
     1364  transform: translateY(5px);
     1365  transition: all 0.3s ease;
     1366  z-index: 1000;
     1367  pointer-events: none;
     1368}
     1369
     1370.fae-info-button:hover .fae-tooltip,
     1371.fae-info-button:focus .fae-tooltip,
     1372.fae-info-button.active .fae-tooltip {
     1373  opacity: 1;
     1374  visibility: visible;
     1375  transform: translateY(0);
     1376  pointer-events: auto;
     1377}
     1378
     1379.fae-tooltip-content {
     1380  padding: 15px;
     1381  font-size: 13px;
     1382  color: #666;
     1383  line-height: 1.6;
     1384}
     1385
     1386.fae-tooltip-content strong {
     1387  color: #333;
     1388  display: block;
     1389  margin-bottom: 8px;
     1390  font-size: 14px;
     1391}
     1392
     1393.fae-tooltip-content p {
     1394  margin: 0;
     1395  color: #666;
     1396}
     1397
     1398/* Tooltip Arrow */
     1399.fae-tooltip::after {
     1400  content: '';
     1401  position: absolute;
     1402  top: 100%;
     1403  right: 20px;
     1404  width: 0;
     1405  height: 0;
     1406  border-left: 8px solid transparent;
     1407  border-right: 8px solid transparent;
     1408  border-top: 8px solid white;
     1409}
     1410
     1411
     1412
     1413 /* Inline Tooltip (for settings) */
     1414.fae-settings-header .fae-tooltip-inline {
     1415  bottom: calc(100% + 8px);
     1416  left: auto;
     1417  right: 0;
     1418  min-width: 250px;
     1419  max-width: 300px;
     1420}
     1421
     1422.fae-settings-header .fae-tooltip-inline::after {
     1423  top: 100%;
     1424  left: auto;
     1425  right: 20px;
     1426}
     1427
     1428
     1429 /* Inline Tooltip (for settings) */
     1430.fae-settings-section .fae-tooltip-inline {
     1431  bottom: calc(100% + 8px);
     1432  left: 0;
     1433  right: auto;
     1434  min-width: 250px;
     1435  max-width: 300px;
     1436}
     1437
     1438.fae-settings-section .fae-tooltip-inline::after {
     1439  top: 100%;
     1440  left: 20px;
     1441  right: auto;
     1442}
     1443
     1444
     1445
     1446
     1447
     1448
     1449/* Hide Default Cursor Toggle Switch */
     1450.fae-hide-cursor-toggle-wrapper {
     1451  display: flex;
     1452  align-items: center;
     1453  gap: 6px;
     1454  margin-left: auto;
     1455}
     1456
     1457.fae-toggle-switch {
     1458  position: relative;
     1459  display: inline-flex;
     1460  align-items: center;
     1461  gap: 8px;
     1462  cursor: pointer;
     1463  user-select: none;
     1464}
     1465
     1466.fae-toggle-input {
     1467  position: absolute;
     1468  opacity: 0;
     1469  width: 0;
     1470  height: 0;
     1471}
     1472
     1473.fae-toggle-slider {
     1474  position: relative;
     1475  display: inline-block;
     1476  width: 36px;
     1477  height: 20px;
     1478  background-color: #ccc;
     1479  border-radius: 20px;
     1480  transition: background-color 0.3s ease;
     1481  flex-shrink: 0;
     1482}
     1483
     1484.fae-toggle-slider:before {
     1485  content: "";
     1486  position: absolute;
     1487  height: 14px;
     1488  width: 14px;
     1489  left: 3px;
     1490  bottom: 3px;
     1491  background-color: white;
     1492  border-radius: 50%;
     1493  transition: transform 0.3s ease, box-shadow 0.3s ease;
     1494  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
     1495}
     1496
     1497.fae-toggle-input:checked + .fae-toggle-slider {
     1498  background-color: #667eea;
     1499}
     1500
     1501.fae-toggle-input:checked + .fae-toggle-slider:before {
     1502  transform: translateX(16px);
     1503  box-shadow: 0 1px 4px rgba(102, 126, 234, 0.4);
     1504}
     1505
     1506.fae-toggle-input:focus + .fae-toggle-slider {
     1507  outline: 2px solid #667eea;
     1508  outline-offset: 2px;
     1509}
     1510
     1511.fae-toggle-label {
     1512  font-size: 14px;
     1513  font-weight: 600;
     1514  color: #333;
     1515  white-space: nowrap;
     1516}
     1517
     1518.fae-toggle-input:checked ~ .fae-toggle-label {
     1519  color: #667eea;
     1520}
     1521
     1522/* Responsive adjustments for toggle */
     1523@media (max-width: 768px) {
     1524  .fae-settings-header {
     1525    flex-direction: column;
     1526    align-items: flex-start !important;
     1527  }
     1528 
     1529  .fae-hide-cursor-toggle-wrapper {
     1530    margin-left: 0;
     1531    width: 100%;
     1532    justify-content: space-between;
     1533  }
     1534
     1535  .fae-tabs-nav {
     1536    flex-direction: column;
     1537    gap: 0;
     1538  }
     1539
     1540  .fae-tab-button {
     1541    width: 100%;
     1542    justify-content: center;
     1543    border-bottom: 2px solid #e1e5e9;
     1544    border-left: 3px solid transparent;
     1545    margin-bottom: 0;
     1546    padding: 12px 20px;
     1547  }
     1548
     1549  .fae-tab-button.active {
     1550    border-bottom-color: #e1e5e9;
     1551    border-left-color: #667eea;
     1552  }
     1553}
     1554
     1555/* Tabs Wrapper */
     1556.fae-tabs-wrapper {
     1557  display: flex;
     1558  align-items: center;
     1559  justify-content: space-between;
     1560  gap: 20px;
     1561  margin-bottom: 20px;
     1562  flex-wrap: wrap;
     1563}
     1564
     1565/* Tabs Navigation */
     1566.fae-tabs-nav {
     1567  display: flex;
     1568  gap: 10px;
     1569  border-bottom: 2px solid #e1e5e9;
     1570  padding-bottom: 0;
     1571  flex: 1;
     1572}
     1573
     1574.fae-tabs-actions {
     1575  display: flex;
     1576  align-items: center;
     1577  gap: 10px;
     1578}
     1579
     1580.fae-tab-button {
     1581  background: transparent;
     1582  border: none;
     1583  padding: 15px 25px;
     1584  position: relative;
     1585  font-size: 16px;
     1586  font-weight: 600;
     1587  color: #666;
     1588  cursor: pointer;
     1589  display: flex;
     1590  align-items: center;
     1591  gap: 10px;
     1592  border-bottom: 3px solid transparent;
     1593  margin-bottom: -2px;
     1594  transition: all 0.3s ease;
     1595  position: relative;
     1596  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
     1597}
     1598
     1599.fae-tab-button:hover {
     1600  color: #667eea;
     1601  background: rgba(102, 126, 234, 0.05);
     1602}
     1603
     1604.fae-tab-button.active {
     1605  color: #667eea;
     1606  border-bottom-color: #667eea;
     1607  background: transparent;
     1608}
     1609
     1610.fae-tab-button .fae-icon {
     1611  width: 20px;
     1612  height: 20px;
     1613  fill: currentColor;
     1614}
     1615
     1616/* Tab Status Indicator */
     1617.fae-tab-status {
     1618  display: inline-flex;
     1619  align-items: center;
     1620  margin-left: 8px;
     1621  vertical-align: middle;
     1622}
     1623
     1624.fae-status-dot {
     1625  width: 8px;
     1626  height: 8px;
     1627  border-radius: 50%;
     1628  background: #dc3545;
     1629  display: inline-block;
     1630  transition: all 0.3s ease;
     1631  box-shadow: 0 0 0 0 rgba(220, 53, 69, 0);
     1632}
     1633
     1634.fae-tab-status.active .fae-status-dot {
     1635  background: #4caf50;
     1636  box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
     1637  animation: pulse 2s infinite;
     1638}
     1639
     1640@keyframes pulse {
     1641  0%, 100% {
     1642    box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4);
     1643  }
     1644  50% {
     1645    box-shadow: 0 0 0 4px rgba(76, 175, 80, 0);
     1646  }
     1647}
     1648
     1649.fae-tab-content {
     1650  display: none;
     1651}
     1652
     1653.fae-tab-content.active {
     1654  display: block;
     1655  animation: fae-tab-fade-in 0.3s ease;
     1656}
     1657
     1658@keyframes fae-tab-fade-in {
     1659  from {
     1660    opacity: 0;
     1661    transform: translateY(10px);
     1662  }
     1663  to {
     1664    opacity: 1;
     1665    transform: translateY(0);
     1666  }
     1667}
     1668
     1669/* Keyboard Effects Section */
     1670.fae-keyboard-effects-panel {
     1671  margin-top: 0;
     1672}
     1673
     1674.fae-keyboard-effect-grid {
     1675  min-height: 200px;
     1676}
     1677
     1678.fae-keyboard-effect-placeholder {
     1679  grid-column: 1 / -1;
     1680  display: flex;
     1681  align-items: center;
     1682  justify-content: center;
     1683  padding: 60px 20px;
     1684  border: 2px dashed #e1e5e9;
     1685  border-radius: 8px;
     1686  background: #fafbfc;
     1687  transition: all 0.3s ease;
     1688}
     1689
     1690.fae-keyboard-effect-placeholder:hover {
     1691  border-color: #667eea;
     1692  background: #f8f9ff;
     1693}
     1694
     1695.fae-placeholder-content {
     1696  display: flex;
     1697  flex-direction: column;
     1698  align-items: center;
     1699  justify-content: center;
     1700  text-align: center;
     1701}
     1702
     1703.fae-placeholder-content .fae-icon {
     1704  fill: #667eea;
     1705}
     1706
     1707.fae-keyboard-effect-settings {
     1708  margin-top: 25px;
     1709}
     1710
     1711/* ═══════════════════════════════════════════════════════════════ */
     1712/* STICKY SIDEBAR LAYOUT */
     1713/* ═══════════════════════════════════════════════════════════════ */
     1714
     1715.fae-split-layout {
     1716  display: grid;
     1717  grid-template-columns: 1fr 340px;
     1718  gap: 24px;
     1719  align-items: start;
     1720}
     1721
     1722@media (max-width: 1100px) {
     1723  .fae-split-layout {
     1724    grid-template-columns: 1fr;
     1725  }
     1726}
     1727
     1728.fae-main-content {
     1729  min-width: 0;
     1730}
     1731
     1732/* Sticky Sidebar */
     1733.fae-sidebar-preview {
     1734  position: sticky;
     1735  top: 42px; /* Below WP admin bar */
     1736  max-height: calc(100vh - 62px);
     1737  overflow: visible;
     1738}
     1739
     1740@media (max-width: 1100px) {
     1741  .fae-sidebar-preview {
     1742    position: static;
     1743    max-height: none;
     1744  }
     1745}
     1746
     1747.fae-preview-card {
     1748  background: #ffffff;
     1749  border-radius: 16px;
     1750  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
     1751  border: 1px solid #e8eaed;
     1752  overflow: visible;
     1753}
     1754
     1755/* Preview Section */
     1756.fae-preview-section {
     1757  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
     1758  border-radius: 16px 16px 0 0;
     1759  overflow: hidden;
     1760}
     1761
     1762.fae-preview-header-inline {
     1763  padding: 14px 18px;
     1764  background: rgba(255,255,255,0.05);
     1765  border-bottom: 1px solid rgba(255,255,255,0.1);
     1766  font-size: 11px;
     1767  font-weight: 700;
     1768  color: rgba(255,255,255,0.6);
     1769  text-transform: uppercase;
     1770  letter-spacing: 1px;
     1771  display: flex;
     1772  align-items: center;
     1773  justify-content: space-between;
     1774}
     1775
     1776.fae-preview-header-actions {
     1777  display: flex;
     1778  align-items: center;
     1779  gap: 8px;
     1780}
     1781
     1782.fae-preview-bg-toggle {
     1783  position: relative;
     1784  width: 44px;
     1785  height: 24px;
     1786  padding: 0;
     1787  background: rgba(255,255,255,0.15);
     1788  border: 1px solid rgba(255,255,255,0.2);
     1789  border-radius: 12px;
     1790  cursor: pointer;
     1791  transition: all 0.3s ease;
     1792  outline: none;
     1793  overflow: hidden;
     1794}
     1795
     1796.fae-preview-bg-toggle:hover {
     1797  background: rgba(255,255,255,0.2);
     1798  border-color: rgba(255,255,255,0.3);
     1799}
     1800
     1801.fae-preview-bg-toggle:focus {
     1802  border-color: #667eea;
     1803  box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.3);
     1804}
     1805
     1806/* .fae-preview-bg-toggle[data-bg="light"] {
     1807  background: rgba(255, 193, 7, 0.3);
     1808  border-color: rgba(255, 193, 7, 0.5);
     1809} */
     1810
     1811.fae-bg-toggle-icon {
     1812  position: absolute;
     1813  top: 50%;
     1814  transform: translateY(-50%);
     1815  width: 18px;
     1816  height: 18px;
     1817  display: flex;
     1818  align-items: center;
     1819  justify-content: center;
     1820  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
     1821  color: rgba(255,255,255,0.9);
     1822}
     1823
     1824.fae-bg-toggle-icon svg {
     1825  width: 14px;
     1826  height: 14px;
     1827  stroke: currentColor;
     1828}
     1829
     1830.fae-bg-icon-moon {
     1831  left: 3px;
     1832  opacity: 1;
     1833  transform: translateY(-50%) scale(1) rotate(0deg);
     1834}
     1835
     1836.fae-bg-icon-sun {
     1837  right: 3px;
     1838  opacity: 0;
     1839  transform: translateY(-50%) scale(0.5) rotate(90deg);
     1840}
     1841
     1842 .fae-preview-bg-toggle[data-bg="light"] .fae-bg-icon-moon {
     1843  opacity: 0;
     1844  transform: translateY(-50%) scale(0.5) rotate(-90deg);
     1845}
     1846
     1847.fae-preview-bg-toggle[data-bg="light"] .fae-bg-icon-sun {
     1848  opacity: 1;
     1849  transform: translateY(-50%) scale(1) rotate(0deg);
     1850  color: #ffc107;
     1851}
     1852
     1853/* Expand Button */
     1854.fae-expand-btn {
     1855  display: flex;
     1856  align-items: center;
     1857  gap: 5px;
     1858  padding: 5px 10px;
     1859  background: rgba(255,255,255,0.1);
     1860  border: 1px solid rgba(255,255,255,0.15);
     1861  border-radius: 5px;
     1862  color: rgba(255,255,255,0.7);
     1863  font-size: 10px;
     1864  font-weight: 600;
     1865  cursor: pointer;
     1866  transition: all 0.2s;
     1867  text-transform: uppercase;
     1868  letter-spacing: 0.5px;
     1869}
     1870.fae-expand-btn:hover {
     1871  background: rgba(255,255,255,0.15);
     1872  color: #fff;
     1873}
     1874.fae-expand-btn svg {
     1875  width: 12px;
     1876  height: 12px;
     1877  fill: currentColor;
     1878}
     1879
     1880/* Fullscreen Preview Modal */
     1881.fae-preview-modal {
     1882  display: none;
     1883  position: fixed;
     1884  top: 0;
     1885  left: 0;
     1886  right: 0;
     1887  bottom: 0;
     1888  background: rgba(10,10,20,0.95);
     1889  z-index: 99999;
     1890  padding: 20px;
     1891}
     1892.fae-preview-modal.active {
     1893  display: flex;
     1894  flex-direction: column;
     1895}
     1896.fae-preview-modal-header {
     1897  display: flex;
     1898  align-items: center;
     1899  justify-content: space-between;
     1900  padding: 15px 20px;
     1901  background: rgba(255,255,255,0.05);
     1902  border-radius: 12px 12px 0 0;
     1903}
     1904.fae-preview-modal-title {
     1905  color: #fff;
     1906  font-size: 14px;
     1907  font-weight: 600;
     1908}
     1909.fae-preview-modal-close {
     1910  display: flex;
     1911  align-items: center;
     1912  gap: 6px;
     1913  padding: 8px 16px;
     1914  background: rgba(255,255,255,0.1);
     1915  border: 1px solid rgba(255,255,255,0.2);
     1916  border-radius: 6px;
     1917  color: #fff;
     1918  font-size: 12px;
     1919  font-weight: 600;
     1920  cursor: pointer;
     1921  transition: all 0.2s;
     1922}
     1923.fae-preview-modal-close:hover {
     1924  background: rgba(239,68,68,0.3);
     1925  border-color: rgba(239,68,68,0.5);
     1926}
     1927.fae-preview-modal-close svg {
     1928  width: 14px;
     1929  height: 14px;
     1930  fill: currentColor;
     1931}
     1932.fae-preview-modal-body {
     1933  flex: 1;
     1934  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
     1935  border-radius: 0 0 12px 12px;
     1936  position: relative;
     1937  min-height: 0;
     1938}
     1939.fae-preview-modal-iframe {
     1940  position: absolute;
     1941  top: 0;
     1942  left: 0;
     1943  width: 100%;
     1944  height: 100%;
     1945  border: none;
     1946  border-radius: 0 0 12px 12px;
     1947}
     1948.fae-preview-modal-hint {
     1949  position: absolute;
     1950  bottom: 20px;
     1951  left: 50%;
     1952  transform: translateX(-50%);
     1953  padding: 8px 16px;
     1954  background: rgba(0,0,0,0.5);
     1955  border-radius: 20px;
     1956  color: rgba(255,255,255,0.5);
     1957  font-size: 12px;
     1958}
     1959
     1960.fae-preview-iframe-wrapper {
     1961  position: relative;
     1962  height: 280px;
     1963}
     1964
     1965.fae-preview-iframe {
     1966  position: absolute;
     1967  top: 0;
     1968  left: 0;
     1969  width: 100%;
     1970  height: 100%;
     1971  border: none;
     1972  opacity: 0;
     1973  transition: opacity 0.2s ease-in;
     1974}
     1975
     1976.fae-preview-iframe.loaded {
     1977  opacity: 1;
     1978}
     1979
     1980/* Appearance Section */
     1981.fae-appearance-section {
     1982  background: #fafbff;
     1983  border-radius: 0 0 16px 16px;
     1984  overflow: visible;
     1985}
     1986
     1987.fae-appearance-section .fae-preview-header-inline {
     1988  background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
     1989  color: #4b5563;
     1990  border-bottom: 1px solid #e8eaed;
     1991  border-top: 1px solid #e8eaed;
     1992}
     1993
     1994.fae-appearance-settings {
     1995  padding: 18px;
     1996  display: flex;
     1997  flex-direction: column;
     1998  gap: 16px;
     1999  overflow: visible;
     2000}
     2001
     2002/* Hide old container styles */
     2003.fae-preview-appearance-container {
     2004  display: none !important;
     2005}
     2006
     2007.fae-inline-setting {
     2008  display: flex;
     2009  flex-direction: column;
     2010  gap: 6px;
     2011}
     2012
     2013.fae-inline-setting label {
     2014  font-size: 11px;
     2015  font-weight: 600;
     2016  color: #6b7280;
     2017  text-transform: uppercase;
     2018  letter-spacing: 0.3px;
     2019}
     2020
     2021.fae-inline-setting select {
     2022  padding: 8px 12px;
     2023  border: 2px solid #e5e7eb;
     2024  border-radius: 6px;
     2025  font-size: 13px;
     2026  background: white;
     2027  cursor: pointer;
     2028  transition: border-color 0.2s;
     2029}
     2030
     2031.fae-inline-setting select:hover {
     2032  border-color: #c7d2fe;
     2033}
     2034
     2035.fae-inline-setting select:focus {
     2036  outline: none;
     2037  border-color: #667eea;
     2038}
     2039
     2040/* Color Input Inline */
     2041.fae-color-input-inline {
     2042  display: flex;
     2043  gap: 8px;
     2044}
     2045
     2046.fae-color-picker-inline {
     2047  width: 44px;
     2048  height: 36px;
     2049  border: 2px solid #e5e7eb;
     2050  border-radius: 6px;
     2051  cursor: pointer;
     2052  padding: 2px;
     2053}
     2054
     2055.fae-color-input-inline input[type="text"] {
     2056  flex: 1;
     2057  padding: 8px 10px;
     2058  border: 2px solid #e5e7eb;
     2059  border-radius: 6px;
     2060  font-size: 13px;
     2061  font-family: 'SF Mono', monospace;
     2062}
     2063
     2064.fae-color-input-inline input[type="text"]:focus {
     2065  outline: none;
     2066  border-color: #667eea;
     2067}
     2068
     2069/* Icon Picker Inline */
     2070.fae-icon-picker-inline {
     2071  position: relative;
     2072  z-index: 50;
     2073}
     2074
     2075.fae-icon-trigger {
     2076  width: 100%;
     2077  padding: 8px 12px;
     2078  background: white;
     2079  border: 2px solid #e5e7eb;
     2080  border-radius: 6px;
     2081  cursor: pointer;
     2082  display: flex;
     2083  align-items: center;
     2084  gap: 10px;
     2085  transition: border-color 0.2s;
     2086}
     2087
     2088.fae-icon-trigger:hover {
     2089  border-color: #c7d2fe;
     2090}
     2091
     2092.fae-icon-chevron {
     2093  width: 12px;
     2094  height: 12px;
     2095  fill: #9ca3af;
     2096  margin-left: auto;
     2097  transition: transform 0.2s;
     2098}
     2099
     2100.fae-icon-dropdown.active + .fae-icon-trigger .fae-icon-chevron,
     2101.fae-icon-picker-inline:has(.fae-icon-dropdown.active) .fae-icon-chevron {
     2102  transform: rotate(180deg);
     2103}
     2104
     2105.fae-icon-preview-small {
     2106  width: 20px;
     2107  height: 20px;
     2108  display: flex;
     2109  align-items: center;
     2110  justify-content: center;
     2111}
     2112
     2113.fae-icon-preview-small svg {
     2114  width: 18px;
     2115  height: 18px;
     2116  fill: #667eea;
     2117}
     2118
     2119.fae-icon-name-small {
     2120  font-size: 13px;
     2121  color: #374151;
     2122}
     2123
     2124/* Icon Dropdown - Opens UPWARD */
     2125.fae-icon-dropdown {
     2126  position: absolute;
     2127  bottom: 100%;
     2128  left: 0;
     2129  right: 0;
     2130  margin-bottom: 4px;
     2131  background: white;
     2132  border: 1px solid #d1d5db;
     2133  border-radius: 8px;
     2134  box-shadow: 0 -10px 40px rgba(0,0,0,0.2);
     2135  z-index: 9999;
     2136  display: none;
     2137  max-height: 240px;
     2138  overflow-y: auto;
     2139}
     2140
     2141.fae-icon-dropdown.active {
     2142  display: block;
     2143}
     2144
     2145/* Ensure parent doesn't clip dropdown */
     2146.fae-appearance-settings {
     2147      overflow: visible !important;
     2148}
     2149
     2150/* .fae-appearance-settings {
     2151    overflow-y: scroll;
     2152    height: 300px;
     2153} */
     2154.fae-appearance-section {
     2155  overflow: visible !important;
     2156}
     2157.fae-icon-picker-inline {
     2158  position: relative;
     2159  z-index: 100;
     2160}
     2161.fae-sidebar-preview {
     2162  overflow: visible !important;
     2163}
     2164.fae-preview-card {
     2165  overflow: visible !important;
     2166}
     2167
     2168.fae-icon-dropdown-grid {
     2169  display: grid;
     2170  grid-template-columns: repeat(5, 1fr);
     2171  gap: 4px;
     2172  padding: 8px;
     2173}
     2174
     2175.fae-icon-dropdown-item {
     2176  width: 36px;
     2177  height: 36px;
     2178  display: flex;
     2179  align-items: center;
     2180  justify-content: center;
     2181  border: 2px solid transparent;
     2182  border-radius: 6px;
     2183  cursor: pointer;
     2184  transition: all 0.15s;
     2185}
     2186
     2187.fae-icon-dropdown-item:hover {
     2188  background: #f3f4f6;
     2189  border-color: #e5e7eb;
     2190}
     2191
     2192.fae-icon-dropdown-item.selected {
     2193  background: #667eea15;
     2194  border-color: #667eea;
     2195}
     2196
     2197.fae-icon-dropdown-item svg {
     2198  width: 20px;
     2199  height: 20px;
     2200  fill: #374151;
     2201}
     2202
     2203/* Flag Picker Styles */
     2204.fae-flag-picker-inline {
     2205  position: relative;
     2206  z-index: 50;
     2207}
     2208
     2209.fae-flag-trigger {
     2210  width: 100%;
     2211  padding: 8px 12px;
     2212  background: white;
     2213  border: 2px solid #e5e7eb;
     2214  border-radius: 6px;
     2215  cursor: pointer;
     2216  display: flex;
     2217  align-items: center;
     2218  gap: 10px;
     2219  transition: border-color 0.2s;
     2220}
     2221
     2222.fae-flag-trigger:hover {
     2223  border-color: #c7d2fe;
     2224}
     2225
     2226.fae-flag-preview-small {
     2227  width: 24px;
     2228  height: 18px;
     2229  display: flex;
     2230  align-items: center;
     2231  justify-content: center;
     2232  flex-shrink: 0;
     2233}
     2234
     2235.fae-flag-preview-small img {
     2236  width: 24px;
     2237  height: 18px;
     2238  object-fit: cover;
     2239  border-radius: 2px;
     2240  border: 1px solid #e5e7eb;
     2241}
     2242
     2243.fae-flag-name-small {
     2244  font-size: 13px;
     2245  color: #374151;
     2246  flex: 1;
     2247  text-align: left;
     2248}
     2249
     2250/* Flag Dropdown */
     2251.fae-flag-dropdown {
     2252  position: absolute;
     2253  bottom: 100%;
     2254  left: 0;
     2255  right: 0;
     2256  margin-bottom: 4px;
     2257  background: white;
     2258  border: 1px solid #d1d5db;
     2259  border-radius: 8px;
     2260  box-shadow: 0 -10px 40px rgba(0,0,0,0.2);
     2261  z-index: 9999;
     2262  display: none;
     2263  max-height: 400px;
     2264  overflow: hidden;
     2265  flex-direction: column;
     2266}
     2267
     2268.fae-flag-dropdown.active {
     2269  display: flex;
     2270}
     2271
     2272.fae-flag-dropdown-header {
     2273  padding: 12px;
     2274  border-bottom: 1px solid #e1e5e9;
     2275}
     2276
     2277.fae-flag-search {
     2278  width: 100%;
     2279  padding: 8px 12px;
     2280  border: 2px solid #e1e5e9;
     2281  border-radius: 6px;
     2282  font-size: 14px;
     2283  transition: border-color 0.3s ease;
     2284}
     2285
     2286.fae-flag-search:focus {
     2287  outline: none;
     2288  border-color: #667eea;
     2289  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
     2290}
     2291
     2292.fae-flag-dropdown-grid {
     2293  padding: 12px;
     2294  display: grid;
     2295  grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
     2296  gap: 8px;
     2297  max-height: 320px;
     2298  overflow-y: auto;
     2299}
     2300
     2301.fae-flag-dropdown-item {
     2302  display: flex;
     2303  flex-direction: column;
     2304  align-items: center;
     2305  padding: 10px 8px;
     2306  border: 2px solid #e1e5e9;
     2307  border-radius: 6px;
     2308  cursor: pointer;
     2309  transition: all 0.2s ease;
     2310  background: white;
     2311  text-align: center;
     2312}
     2313
     2314.fae-flag-dropdown-item:hover {
     2315  border-color: #667eea;
     2316  transform: translateY(-2px);
     2317  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
     2318}
     2319
     2320.fae-flag-dropdown-item.selected {
     2321  border-color: #667eea;
     2322  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     2323}
     2324
     2325.fae-flag-preview-img {
     2326  width: 48px;
     2327  height: 36px;
     2328  object-fit: cover;
     2329  border-radius: 4px;
     2330  border: 1px solid rgba(0,0,0,0.1);
     2331  margin-bottom: 6px;
     2332}
     2333
     2334.fae-flag-dropdown-item.selected .fae-flag-preview-img {
     2335  border-color: rgba(255,255,255,0.5);
     2336}
     2337
     2338.fae-flag-preview-placeholder {
     2339  width: 48px;
     2340  height: 36px;
     2341  display: flex;
     2342  align-items: center;
     2343  justify-content: center;
     2344  background: #f3f4f6;
     2345  border-radius: 4px;
     2346  margin-bottom: 6px;
     2347  border: 1px solid #e5e7eb;
     2348}
     2349
     2350.fae-flag-preview-placeholder svg {
     2351  width: 24px;
     2352  height: 24px;
     2353  stroke: #9ca3af;
     2354}
     2355
     2356.fae-flag-dropdown-item.selected .fae-flag-preview-placeholder {
     2357  background: rgba(255,255,255,0.2);
     2358  border-color: rgba(255,255,255,0.3);
     2359}
     2360
     2361.fae-flag-dropdown-item.selected .fae-flag-preview-placeholder svg {
     2362  stroke: white;
     2363}
     2364
     2365.fae-flag-label {
     2366  display: none; /* Hide country code label - search uses country names */
     2367  font-size: 10px;
     2368  font-weight: 500;
     2369  color: #374151;
     2370  line-height: 1.2;
     2371  word-break: break-word;
     2372}
     2373
     2374.fae-flag-dropdown-item.selected .fae-flag-label {
     2375  color: white;
     2376}
     2377
     2378/* Scrollbar styling for flag grid */
     2379.fae-flag-dropdown-grid::-webkit-scrollbar {
     2380  width: 6px;
     2381}
     2382
     2383.fae-flag-dropdown-grid::-webkit-scrollbar-track {
     2384  background: #f1f1f1;
     2385  border-radius: 3px;
     2386}
     2387
     2388.fae-flag-dropdown-grid::-webkit-scrollbar-thumb {
     2389  background: #c1c1c1;
     2390  border-radius: 3px;
     2391}
     2392
     2393.fae-flag-dropdown-grid::-webkit-scrollbar-thumb:hover {
     2394  background: #a8a8a8;
     2395}
     2396
     2397/* Pro Badge Styles */
     2398.fae-pro-badge {
     2399  display: inline-block;
     2400  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
     2401  color: white;
     2402  font-size: 9px;
     2403  font-weight: 700;
     2404  padding: 2px 6px;
     2405  border-radius: 3px;
     2406  text-transform: uppercase;
     2407  letter-spacing: 0.5px;
     2408  margin-left: 6px;
     2409  vertical-align: middle;
     2410  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
     2411}
     2412
     2413.fae-effect-option .fae-pro-badge {
     2414  position: static;
     2415  font-size: 9px;
     2416  font-weight: 700;
     2417  padding: 3px 6px;
     2418  border-radius: 4px;
     2419  text-transform: uppercase;
     2420  letter-spacing: 0.5px;
     2421  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
     2422  z-index: 10;
     2423  pointer-events: none;
     2424  line-height: 1.2;
     2425  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     2426  color: white;
     2427  white-space: nowrap;
     2428  display: inline-flex;
     2429  align-items: center;
     2430  justify-content: center;
     2431  margin-bottom: 4px;
     2432  order: -1;
     2433}
     2434
     2435/* Match multi-color setting pro badge to disabled effects style */
     2436.fae-multi-color-setting .fae-pro-badge {
     2437  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     2438  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
     2439  padding: 3px 6px;
     2440  border-radius: 4px;
     2441}
     2442
     2443/* Match interactive cursor setting pro badge to effect option style */
     2444.fae-interactive-cursor-setting .fae-pro-badge {
     2445  position: static;
     2446  font-size: 9px;
     2447  font-weight: 700;
     2448  padding: 3px 6px;
     2449  border-radius: 4px;
     2450  text-transform: uppercase;
     2451  letter-spacing: 0.5px;
     2452  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
     2453  z-index: 10;
     2454  pointer-events: none;
     2455  line-height: 1.2;
     2456  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     2457  color: white;
     2458  white-space: nowrap;
     2459  display: inline-flex;
     2460  align-items: center;
     2461  justify-content: center;
     2462  margin-left: 6px;
     2463  vertical-align: middle;
     2464}
     2465
     2466/* Pro Locked Effect Styles */
     2467.fae-effect-pro-locked {
     2468  position: relative;
     2469  cursor: not-allowed !important;
     2470  pointer-events: auto;
     2471  border-color: #e1e5e9 !important;
     2472  opacity: 0.7;
     2473}
     2474
     2475.fae-effect-pro-locked::before {
     2476  content: '';
     2477  position: absolute;
     2478  top: 0;
     2479  left: 0;
     2480  right: 0;
     2481  bottom: 0;
     2482  background: linear-gradient(135deg, rgba(102, 126, 234, 0.06) 0%, rgba(118, 75, 162, 0.06) 100%);
     2483  border-radius: 8px;
     2484  z-index: 1;
     2485  pointer-events: none;
     2486  border: 1px dashed rgba(102, 126, 234, 0.25);
     2487}
     2488
     2489.fae-effect-pro-locked .fae-effect-content {
     2490  position: relative;
     2491  z-index: 0;
     2492}
     2493
     2494.fae-effect-pro-locked .fae-effect-letter {
     2495  display: none;
     2496  visibility: hidden;
     2497}
     2498
     2499.fae-effect-pro-locked .fae-effect-name {
     2500  opacity: 0.6;
     2501  color: #64748b;
     2502  order: 1;
     2503}
     2504
     2505.fae-effect-pro-locked:hover {
     2506  transform: none !important;
     2507  box-shadow: none !important;
     2508  border-color: #e1e5e9 !important;
     2509}
     2510
     2511.fae-effect-pro-locked:hover::before {
     2512  border-color: rgba(102, 126, 234, 0.35);
     2513}
     2514
     2515.fae-effect-pro-locked .fae-pro-badge {
     2516  z-index: 10;
     2517  opacity: 1;
     2518  filter: none;
     2519}
     2520
     2521/* Upgrade Notice Styles */
     2522.fae-upgrade-notice {
     2523  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     2524  border-radius: 8px;
     2525  padding: 16px;
     2526  margin: 16px 0;
     2527  color: white;
     2528}
     2529
     2530.fae-upgrade-notice-content {
     2531  display: flex;
     2532  align-items: center;
     2533  gap: 16px;
     2534}
     2535
     2536.fae-upgrade-icon {
     2537  width: 32px;
     2538  height: 32px;
     2539  flex-shrink: 0;
     2540  stroke: white;
     2541}
     2542
     2543.fae-upgrade-text {
     2544  flex: 1;
     2545}
     2546
     2547.fae-upgrade-text strong {
     2548  display: block;
     2549  font-size: 14px;
     2550  margin-bottom: 4px;
     2551}
     2552
     2553.fae-upgrade-text p {
     2554  margin: 0;
     2555  font-size: 12px;
     2556  opacity: 0.9;
     2557}
     2558
     2559.fae-upgrade-button {
     2560  background: white;
     2561  color: #667eea;
     2562  padding: 8px 16px;
     2563  border-radius: 6px;
     2564  text-decoration: none;
     2565  font-weight: 600;
     2566  font-size: 13px;
     2567  transition: transform 0.2s, box-shadow 0.2s;
     2568  white-space: nowrap;
     2569  flex-shrink: 0;
     2570}
     2571
     2572.fae-upgrade-button:hover {
     2573  transform: translateY(-1px);
     2574  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
     2575  color: #764ba2;
     2576}
     2577
     2578/* Effect Type Disabled Styles (Free Version Restriction) */
     2579/* Effects appear normal but show upgrade modal when clicked */
     2580.fae-effect-type-disabled {
     2581  position: relative;
     2582  cursor: pointer !important;
     2583  pointer-events: auto;
     2584  /* No visual dimming - keep completely normal appearance */
     2585}
     2586
     2587/* Upgrade Notice Modal */
     2588.fae-upgrade-modal {
     2589  position: fixed;
     2590  top: 0;
     2591  left: 0;
     2592  right: 0;
     2593  bottom: 0;
     2594  z-index: 100000;
     2595  display: flex;
     2596  align-items: center;
     2597  justify-content: center;
     2598  opacity: 0;
     2599  visibility: hidden;
     2600  transition: opacity 0.3s ease, visibility 0.3s ease;
     2601}
     2602
     2603.fae-upgrade-modal.active {
     2604  opacity: 1;
     2605  visibility: visible;
     2606}
     2607
     2608.fae-upgrade-modal-backdrop {
     2609  position: absolute;
     2610  top: 0;
     2611  left: 0;
     2612  right: 0;
     2613  bottom: 0;
     2614  background: rgba(0, 0, 0, 0.6);
     2615  backdrop-filter: blur(4px);
     2616}
     2617
     2618.fae-upgrade-modal-content {
     2619  position: relative;
     2620  background: white;
     2621  border-radius: 16px;
     2622  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
     2623  max-width: 480px;
     2624  width: 90%;
     2625  max-height: 90vh;
     2626  overflow-y: auto;
     2627  transform: scale(0.95);
     2628  transition: transform 0.3s ease;
     2629  z-index: 1;
     2630}
     2631
     2632.fae-upgrade-modal.active .fae-upgrade-modal-content {
     2633  transform: scale(1);
     2634}
     2635
     2636.fae-upgrade-modal-close {
     2637  position: absolute;
     2638  top: 16px;
     2639  right: 16px;
     2640  background: #f1f5f9;
     2641  border: none;
     2642  border-radius: 8px;
     2643  width: 32px;
     2644  height: 32px;
     2645  display: flex;
     2646  align-items: center;
     2647  justify-content: center;
     2648  cursor: pointer;
     2649  color: #64748b;
     2650  transition: all 0.2s ease;
     2651  z-index: 10;
     2652}
     2653
     2654.fae-upgrade-modal-close:hover {
     2655  background: #e2e8f0;
     2656  color: #475569;
     2657  transform: rotate(90deg);
     2658}
     2659
     2660.fae-upgrade-modal-body {
     2661  padding: 40px;
     2662  text-align: center;
     2663}
     2664
     2665.fae-upgrade-modal-body .fae-upgrade-icon {
     2666  width: 64px;
     2667  height: 64px;
     2668  margin: 0 auto 24px;
     2669  stroke: #667eea;
     2670  stroke-width: 2;
     2671}
     2672
     2673.fae-upgrade-modal-body h3 {
     2674  font-size: 24px;
     2675  font-weight: 700;
     2676  color: #1e293b;
     2677  margin: 0 0 12px;
     2678}
     2679
     2680.fae-upgrade-modal-body p {
     2681  font-size: 15px;
     2682  line-height: 1.6;
     2683  color: #64748b;
     2684  margin: 0 0 32px;
     2685}
     2686
     2687.fae-upgrade-modal-body .fae-upgrade-button {
     2688  display: inline-block;
     2689  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     2690  color: white;
     2691  padding: 12px 32px;
     2692  border-radius: 8px;
     2693  text-decoration: none;
     2694  font-weight: 600;
     2695  font-size: 15px;
     2696  transition: transform 0.2s, box-shadow 0.2s;
     2697}
     2698
     2699.fae-upgrade-modal-body .fae-upgrade-button:hover {
     2700  transform: translateY(-2px);
     2701  box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
     2702  color: white;
     2703}
     2704
     2705@media (max-width: 640px) {
     2706  .fae-upgrade-modal-content {
     2707    width: 95%;
     2708    margin: 20px;
     2709  }
     2710 
     2711  .fae-upgrade-modal-body {
     2712    padding: 32px 24px;
     2713  }
     2714 
     2715  .fae-upgrade-modal-body h3 {
     2716    font-size: 20px;
     2717  }
     2718 
     2719  .fae-upgrade-modal-body p {
     2720    font-size: 14px;
     2721  }
     2722}
     2723
     2724/* ========================================
     2725   LIMITED CUSTOMIZATION - LOCKED STYLES
     2726   ======================================== */
     2727
     2728/* Inline Pro badge for limited customization */
     2729.fae-pro-badge-inline {
     2730  display: inline-flex;
     2731  align-items: center;
     2732  padding: 2px 8px;
     2733  font-size: 10px;
     2734  font-weight: 700;
     2735  border-radius: 4px;
     2736  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
     2737  line-height: 1.2;
     2738  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     2739  color: white;
     2740  white-space: nowrap;
     2741  margin-left: 6px;
     2742  vertical-align: middle;
     2743}
     2744
     2745/* Locked color swatch for free users */
     2746.fae-color-locked {
     2747  display: flex;
     2748  align-items: center;
     2749  gap: 8px;
     2750}
     2751
     2752.fae-color-swatch-locked {
     2753  width: 32px;
     2754  height: 32px;
     2755  border-radius: 6px;
     2756  border: 2px solid #e5e7eb;
     2757  cursor: not-allowed;
     2758  position: relative;
     2759  flex-shrink: 0;
     2760}
     2761
     2762.fae-color-swatch-locked::after {
     2763  content: '';
     2764  position: absolute;
     2765  top: 50%;
     2766  left: 50%;
     2767  transform: translate(-50%, -50%);
     2768  width: 16px;
     2769  height: 16px;
     2770  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'%3E%3C/path%3E%3C/svg%3E");
     2771  background-size: contain;
     2772  background-repeat: no-repeat;
     2773  opacity: 0.8;
     2774}
     2775
     2776.fae-color-locked-text {
     2777  font-family: 'SF Mono', SFMono-Regular, ui-monospace, monospace;
     2778  font-size: 12px;
     2779  color: #9ca3af;
     2780  text-transform: uppercase;
     2781}
     2782
     2783/* Locked speed select for free users */
     2784.fae-speed-locked {
     2785  background-color: #f9fafb;
     2786  cursor: not-allowed;
     2787}
     2788
     2789.fae-speed-locked option:disabled {
     2790  color: #9ca3af;
     2791  font-style: italic;
     2792}
     2793
     2794/* Show Pro badge on color setting when customization is limited */
     2795.fae-color-setting[data-limited-customization="1"] .fae-pro-badge-inline {
     2796  display: inline-block;
     2797}
     2798
     2799/* Hide Pro badge when customization is not limited */
     2800.fae-color-setting[data-limited-customization="0"] .fae-pro-badge-inline {
     2801  display: none;
     2802}
     2803
     2804/* Plugin title + version below settings panel (bottom left) */
     2805.fae-dashboard-footer-version {
     2806  margin: 20px 0 0 0;
     2807  padding: 0;
     2808  font-size: 13px;
     2809  color: #646970;
     2810  text-align: left;
     2811}
     2812
  • faecursor/trunk/assets/js/fae-cursor-admin.js

    r3384653 r3454888  
    11/**
    22 * FaeCursor Admin Dashboard JavaScript
    3  * Handles live preview, settings management, and interactive features
     3 * Handles settings management and interactive features
    44 */
    55
     
    77  "use strict";
    88
     9  // Debounce utility function (with browser compatibility)
     10  const debounce = (func, wait) => {
     11    let timeout;
     12    return function executedFunction() {
     13      const args = arguments;
     14      const context = this;
     15      const later = function() {
     16        clearTimeout(timeout);
     17        func.apply(context, args);
     18      };
     19      clearTimeout(timeout);
     20      timeout = setTimeout(later, wait);
     21    };
     22  };
     23
     24  // Fetch polyfill check for older browsers
     25  if (typeof fetch === 'undefined') {
     26    // If fetch is not available, use XMLHttpRequest as fallback
     27    window.fetch = function(url) {
     28      return new Promise(function(resolve, reject) {
     29        const xhr = new XMLHttpRequest();
     30        xhr.open('GET', url);
     31        xhr.onload = function() {
     32          if (xhr.status >= 200 && xhr.status < 300) {
     33            resolve({
     34              ok: true,
     35              status: xhr.status,
     36              text: function() { return Promise.resolve(xhr.responseText); }
     37            });
     38          } else {
     39            reject(new Error('HTTP ' + xhr.status));
     40          }
     41        };
     42        xhr.onerror = reject;
     43        xhr.send();
     44      });
     45    };
     46  }
     47
     48  // Effects with FULL customization (no color/speed limits)
     49  // All other cursor effects have limited customization for free users
     50  const FULL_CUSTOMIZATION_EFFECTS = [
     51    'none',
     52    'drop-effect',
     53    'rise-effect',
     54    'line-effect',
     55    'duo-circle',
     56    'duo-circle-2',
     57  ];
     58 
     59  // Keyboard effects with FULL color customization (no limits)
     60  // All other keyboard effects have limited color for free users
     61  const KEYBOARD_FULL_COLOR_EFFECTS = [
     62    'none',
     63    'sparkle-keys', // Uses multi-color feature instead
     64  ];
     65 
     66  // Particle effects with FULL customization (no limits)
     67  // All other particle effects have limited customization for free users
     68  const PARTICLE_FULL_CUSTOMIZATION_EFFECTS = [
     69    'none',
     70    'particle-10' // Snowfall - fully customizable
     71  ];
     72 
     73  // Default colors for free users on limited effects
     74  const FREE_DEFAULT_COLORS = {
     75    cursor: '#fcba03',
     76    keyboard: '#667eea',
     77    particle: '#a855f7'
     78  };
     79 
    980  // Admin Dashboard Controller
    1081  const FaeAdmin = {
     82    // Store saved effects state (what's actually saved, not form selections)
     83    savedEffects: {
     84      cursor: 'none',
     85      keyboard: 'none',
     86      particle: 'none'
     87    },
     88   
     89    // Store user-selected colors (to preserve when switching between limited/full effects)
     90    userSelectedColors: {
     91      cursor: null,
     92      keyboard: null,
     93      particle: null
     94    },
     95   
     96    // Store user-selected speeds (to preserve when switching between limited/full effects)
     97    userSelectedSpeeds: {
     98      cursor: null,
     99      particle: null
     100    },
     101   
     102    // Track which tabs have shown the upgrade modal (to avoid showing multiple times per tab)
     103    upgradeModalShownForTab: {
     104      cursor: false,
     105      keyboard: false,
     106      particle: false
     107    },
     108   
     109    // Check if Pro plugin is active (from server-side detection)
     110    // Use truthy check to handle PHP type coercion (boolean/integer/string)
     111    isPremium: typeof faeAdminData !== 'undefined' && !!faeAdminData.isPremium,
     112   
     113    // Check if a cursor effect has limited customization
     114    effectHasLimitedCustomization: function(effectId) {
     115      return !FULL_CUSTOMIZATION_EFFECTS.includes(effectId);
     116    },
     117   
     118    // Check if user can customize color for cursor effect (free: only for non-limited effects)
     119    canCustomizeColor: function(effectId) {
     120      if (!this.effectHasLimitedCustomization(effectId)) {
     121        return true;
     122      }
     123      return this.isPremium;
     124    },
     125   
     126    // Check if user can customize speed for cursor effect (free: only for non-limited effects)
     127    canCustomizeSpeed: function(effectId) {
     128      if (!this.effectHasLimitedCustomization(effectId)) {
     129        return true;
     130      }
     131      return this.isPremium;
     132    },
     133   
     134    // Check if a keyboard effect has limited color customization
     135    keyboardEffectHasLimitedColor: function(effectId) {
     136      return !KEYBOARD_FULL_COLOR_EFFECTS.includes(effectId);
     137    },
     138   
     139    // Check if user can customize color for keyboard effect (free: only for non-limited effects)
     140    canCustomizeKeyboardColor: function(effectId) {
     141      if (!this.keyboardEffectHasLimitedColor(effectId)) {
     142        return true;
     143      }
     144      return this.isPremium;
     145    },
     146   
     147    // Check if a particle effect has limited customization
     148    particleEffectHasLimitedCustomization: function(effectId) {
     149      return !PARTICLE_FULL_CUSTOMIZATION_EFFECTS.includes(effectId);
     150    },
     151   
     152    // Check if user can customize color for particle effect (free: only for non-limited effects)
     153    canCustomizeParticleColor: function(effectId) {
     154      if (!this.particleEffectHasLimitedCustomization(effectId)) {
     155        return true;
     156      }
     157      return this.isPremium;
     158    },
     159   
     160    // Check if user can customize speed for particle effect (free: only for non-limited effects)
     161    canCustomizeParticleSpeed: function(effectId) {
     162      if (!this.particleEffectHasLimitedCustomization(effectId)) {
     163        return true;
     164      }
     165      return this.isPremium;
     166    },
     167   
    11168    init: function () {
     169      // Initialize saved effects state from form values (which reflect saved state on page load)
     170      this.savedEffects.cursor = $('input[name="fae_cursor_options[effect]"]:checked').val() || 'none';
     171      this.savedEffects.keyboard = $('input[name="fae_keyboard_options[effect]"]:checked').val() || 'none';
     172      this.savedEffects.particle = $('input[name="fae_particle_options[effect]"]:checked').val() || 'none';
     173     
     174      // Initialize user-selected colors from saved values (to preserve across effect switches)
     175      this.userSelectedColors.cursor = $('#fae-color').val() || $('#cursor-effects-tab input[name="fae_cursor_options[color]"]').val() || FREE_DEFAULT_COLORS.cursor;
     176      this.userSelectedColors.keyboard = $('#fae-keyboard-color').val() || $('#keyboard-effects-tab input[name="fae_keyboard_options[color]"]').val() || FREE_DEFAULT_COLORS.keyboard;
     177      this.userSelectedColors.particle = $('#fae-particle-color').val() || $('#particle-effects-tab input[name="fae_particle_options[color]"]').val() || FREE_DEFAULT_COLORS.particle;
     178     
     179      // Initialize user-selected speeds from saved values
     180      this.userSelectedSpeeds.cursor = $('#fae-speed').val() || 'normal';
     181      this.userSelectedSpeeds.particle = $('#fae-particle-speed').val() || 'normal';
     182     
    12183      this.bindEvents();
    13184      this.updateStats();
    14185      this.loadSettings();
     186      this.updateEffectTypeRestrictions();
    15187    },
    16188
    17189    bindEvents: function () {
    18       // Effect selection
    19       $(".fae-effect-option").on("click", this.handleEffectChange);
     190      // Tab switching
     191      $(".fae-tab-button").on("click", this.handleTabSwitch);
     192
     193      // Cursor effect selection
     194      $("#cursor-effects-tab .fae-effect-option").on("click", function(e) {
     195        // Prevent clicking on Pro locked effects
     196        if ($(this).hasClass('fae-effect-pro-locked')) {
     197          e.preventDefault();
     198          e.stopPropagation();
     199          return false;
     200        }
     201        // Only handle if clicking on the option itself, not on nested elements
     202        if ($(e.target).closest('.fae-effect-option').is($(this))) {
     203          const $radio = $(this).find('input[type="radio"]');
     204          if ($radio.length && !$radio.is(':disabled')) {
     205            const effectValue = $radio.val();
     206            const wasChecked = $radio.is(':checked');
     207           
     208            // Select the effect first (for preview)
     209            $radio.prop('checked', true);
     210           
     211            // Show upgrade modal on first click if another effect type is active
     212            if (effectValue !== 'none' && FaeAdmin.isAnotherEffectTypeActive('cursor')) {
     213              // Only show modal once per tab session
     214              if (!FaeAdmin.upgradeModalShownForTab.cursor) {
     215                FaeAdmin.showUpgradeNoticeModal();
     216                FaeAdmin.upgradeModalShownForTab.cursor = true;
     217              }
     218            }
     219           
     220            // Trigger change event to ensure preview updates (allow preview)
     221            if (!wasChecked) {
     222              $radio.trigger('change');
     223            } else {
     224              // If already checked, call handler directly to update preview
     225              FaeAdmin.handleEffectChange.call($radio[0]);
     226            }
     227          }
     228        }
     229      });
    20230      $('input[name="fae_cursor_options[effect]"]').on(
    21231        "change",
    22232        this.handleEffectChange
    23233      );
     234
     235      // Keyboard effect selection
     236      $('input[name="fae_keyboard_options[effect]"]').on(
     237        "change",
     238        this.handleKeyboardEffectChange
     239      );
     240     
     241      // Also handle keyboard effect option clicks
     242      $("#keyboard-effects-tab .fae-effect-option").on("click", function(e) {
     243        // Prevent clicking on Pro locked effects
     244        if ($(this).hasClass('fae-effect-pro-locked')) {
     245          e.preventDefault();
     246          e.stopPropagation();
     247          return false;
     248        }
     249        const $radio = $(this).find('input[type="radio"]');
     250        if ($radio.length && !$radio.is(':disabled')) {
     251          const effectValue = $radio.val();
     252          const wasChecked = $radio.is(':checked');
     253         
     254          // Select the effect first (for preview)
     255          $radio.prop('checked', true);
     256         
     257          // Show upgrade modal on first click if another effect type is active
     258          if (effectValue !== 'none' && FaeAdmin.isAnotherEffectTypeActive('keyboard')) {
     259            // Only show modal once per tab session
     260            if (!FaeAdmin.upgradeModalShownForTab.keyboard) {
     261              FaeAdmin.showUpgradeNoticeModal();
     262              FaeAdmin.upgradeModalShownForTab.keyboard = true;
     263            }
     264          }
     265         
     266          // Trigger change event to ensure preview updates (allow preview)
     267          if (!wasChecked) {
     268            $radio.trigger('change');
     269          } else {
     270            // If already checked, call handler directly to update preview
     271            FaeAdmin.handleKeyboardEffectChange.call($radio[0]);
     272          }
     273        }
     274      });
     275
     276      // Particle effect selection
     277      $('input[name="fae_particle_options[effect]"]').on(
     278        "change",
     279        this.handleParticleEffectChange
     280      );
     281     
     282      // Also handle particle effect option clicks
     283      $("#particle-effects-tab .fae-effect-option").on("click", function(e) {
     284        // Prevent clicking on Pro locked effects
     285        if ($(this).hasClass('fae-effect-pro-locked')) {
     286          e.preventDefault();
     287          e.stopPropagation();
     288          return false;
     289        }
     290        const $radio = $(this).find('input[type="radio"]');
     291        if ($radio.length && !$radio.is(':disabled')) {
     292          const effectValue = $radio.val();
     293          const wasChecked = $radio.is(':checked');
     294         
     295          // Select the effect first (for preview)
     296          $radio.prop('checked', true);
     297         
     298          // Show upgrade modal on first click if another effect type is active
     299          if (effectValue !== 'none' && FaeAdmin.isAnotherEffectTypeActive('particle')) {
     300            // Only show modal once per tab session
     301            if (!FaeAdmin.upgradeModalShownForTab.particle) {
     302              FaeAdmin.showUpgradeNoticeModal();
     303              FaeAdmin.upgradeModalShownForTab.particle = true;
     304            }
     305          }
     306         
     307          // Trigger change event to ensure preview updates (allow preview)
     308          if (!wasChecked) {
     309            $radio.trigger('change');
     310          } else {
     311            // If already checked, call handler directly to update preview
     312            FaeAdmin.handleParticleEffectChange.call($radio[0]);
     313          }
     314        }
     315      });
    24316
    25317      // Settings changes
     
    28320        this.handleSettingChange
    29321      );
    30 
    31       // Color picker changes
     322     
     323      // Checkbox changes (need special handling) - including toggle switch
     324      $('input[type="checkbox"][name*="fae_cursor_options"]').on(
     325        "change",
     326        this.handleCheckboxChange
     327      );
     328     
     329      // Also handle toggle switch specifically
     330      $('#fae_hide_default_cursor_toggle').on(
     331        "change",
     332        this.handleCheckboxChange
     333      );
     334
     335      // Color picker changes - use 'input' for real-time updates while selecting
     336      $(".fae-color-picker").on("input", this.handleColorChange);
    32337      $(".fae-color-picker").on("change", this.handleColorChange);
    33338
     
    40345      $(document).on("keydown", this.handleKeydown);
    41346
    42       // Form submission
    43       $("#fae-cursor-form").on("submit", this.handleFormSubmit);
    44 
    45       // Reset settings
    46       $(".fae-reset-settings").on("click", this.resetSettings);
     347      // Form submission - bind all three forms
     348      $("#fae-cursor-form, #fae-keyboard-form, #fae-particle-form").on("submit", this.handleFormSubmit);
     349
     350      // Scope type dropdown changes
     351      $(".fae-scope-type-select").on("change", this.handleScopeTypeChange);
     352
     353      // User restriction type radio button changes
     354      $(".fae-user-restriction-type").on("change", this.handleUserRestrictionTypeChange);
     355
     356      // Preview iframe updates
     357      this.initPreviewIframes();
     358
     359      // Info button tooltip (close on outside click)
     360      $(document).on("click", function(e) {
     361        if (!$(e.target).closest('.fae-info-button').length) {
     362          $('.fae-info-button').removeClass('active');
     363        }
     364      });
     365     
     366      // Toggle tooltip on click for mobile/touch devices
     367      $(document).on("click", ".fae-info-button", function(e) {
     368        e.stopPropagation();
     369        $(this).toggleClass('active');
     370      });
     371    },
     372
     373    handleTabSwitch: function (e) {
     374      e.preventDefault();
     375      const $button = $(this);
     376      const targetTab = $button.data("tab");
     377
     378      // Remove active class from all buttons and tabs
     379      $(".fae-tab-button").removeClass("active");
     380      $(".fae-tab-content").removeClass("active");
     381
     382      // Add active class to clicked button and corresponding tab
     383      $button.addClass("active");
     384      $("#" + targetTab + "-tab").addClass("active");
     385     
     386      // Update visibility of settings when switching to keyboard effects tab
     387      if (targetTab === 'keyboard-effects') {
     388        const keyboardEffect = $('input[name="fae_keyboard_options[effect]"]:checked').val() || 'none';
     389        if (keyboardEffect === 'sparkle-keys') {
     390          $('#keyboard-effects-tab .fae-multi-color-setting').show();
     391          $('#keyboard-effects-tab .fae-color-setting').hide();
     392          // Always show color picker for sparkle-keys (multi-color is disabled - Pro feature)
     393          $('#keyboard-effects-tab .fae-color-setting-sparkle').show();
     394        } else {
     395          $('#keyboard-effects-tab .fae-multi-color-setting').hide();
     396          $('#keyboard-effects-tab .fae-color-setting').show();
     397          $('#keyboard-effects-tab .fae-color-setting-sparkle').hide();
     398        }
     399      }
     400     
     401      // Don't reset the upgrade modal flag - it should persist across tab switches
     402      // Modal will only show once per tab per session, even if user switches away and comes back
     403    },
     404
     405    handleKeyboardEffectChange: function () {
     406      const effect = $(this).val();
     407      const effectSettings = $(".fae-keyboard-effect-settings");
     408
     409      // Show/hide settings based on effect
     410      if (effect === "none") {
     411        effectSettings.hide();
     412      } else {
     413        effectSettings.show();
     414      }
     415
     416      // Show/hide multi-color setting for sparkle-keys
     417      if (effect === 'sparkle-keys') {
     418        $('#keyboard-effects-tab .fae-multi-color-setting').show();
     419        $('#keyboard-effects-tab .fae-color-setting').hide();
     420        // Always show color picker for sparkle-keys (multi-color is disabled - Pro feature)
     421        $('#keyboard-effects-tab .fae-color-setting-sparkle').show();
     422      } else {
     423        $('#keyboard-effects-tab .fae-multi-color-setting').hide();
     424        $('#keyboard-effects-tab .fae-color-setting').show();
     425        $('#keyboard-effects-tab .fae-color-setting-sparkle').hide();
     426      }
     427
     428      // Hide/show entire appearance section in preview when effect is 'none'
     429      const $appearanceSection = $('#keyboard-effects-tab .fae-appearance-section');
     430      if (effect === 'none') {
     431        $appearanceSection.hide();
     432      } else {
     433        $appearanceSection.show();
     434      }
     435
     436      // Visually mark selected option within its grid (purple state)
     437      const $option = $(this).closest(".fae-effect-option");
     438      if ($option.length) {
     439        const $grid = $option.closest(".fae-effect-grid");
     440        if ($grid.length) {
     441          $grid.find(".fae-effect-option").removeClass("fae-effect-selected");
     442        } else {
     443          $("#keyboard-effects-tab .fae-effect-option").removeClass("fae-effect-selected");
     444        }
     445        $option.addClass("fae-effect-selected");
     446      }
     447
     448      // Update preview iframe
     449      if (typeof FaeAdmin.updatePreviewIframe === 'function') {
     450        FaeAdmin.updatePreviewIframe('keyboard');
     451      }
     452     
     453      // Update effect type restrictions when effect changes
     454      FaeAdmin.updateEffectTypeRestrictions();
     455     
     456      // Update limited customization UI for keyboard effects
     457      FaeAdmin.updateKeyboardLimitedCustomization(effect);
     458    },
     459   
     460    // Update keyboard effect color UI based on limited customization rules
     461    updateKeyboardLimitedCustomization: function(effect) {
     462      const hasLimitedColor = this.keyboardEffectHasLimitedColor(effect);
     463      const canCustomizeColor = this.canCustomizeKeyboardColor(effect);
     464     
     465      // Update data attributes for CSS
     466      $('#keyboard-effects-tab .fae-keyboard-color-setting').attr('data-limited-customization', hasLimitedColor ? '1' : '0');
     467     
     468      // Update color setting (but not for sparkle-keys which uses multi-color)
     469      if (effect !== 'sparkle-keys') {
     470        const $colorSetting = $('#keyboard-effects-tab .fae-keyboard-color-setting');
     471        const $colorLabel = $colorSetting.find('label');
     472        const $colorInput = $colorSetting.find('.fae-color-input-inline');
     473       
     474        // Update Pro badge in label
     475        if (hasLimitedColor && !canCustomizeColor) {
     476          // SAVE user's current color before locking (if not already the default)
     477          const currentColorVal = $('#fae-keyboard-color').val();
     478          if (currentColorVal && currentColorVal !== FREE_DEFAULT_COLORS.keyboard) {
     479            this.userSelectedColors.keyboard = currentColorVal;
     480          }
     481         
     482          // Add Pro badge if not present
     483          if (!$colorLabel.find('.fae-pro-badge-inline').length) {
     484            $colorLabel.append('<span class="fae-pro-badge fae-pro-badge-inline">PRO</span>');
     485          }
     486          // Replace color input with locked swatch
     487          $colorInput.addClass('fae-color-locked');
     488          $colorInput.html(
     489            '<div class="fae-color-swatch-locked" style="background-color: ' + FREE_DEFAULT_COLORS.keyboard + ';" title="Upgrade to Pro to customize color"></div>' +
     490            '<input type="hidden" name="fae_keyboard_options[color]" value="' + FREE_DEFAULT_COLORS.keyboard + '" id="fae-keyboard-color">' +
     491            '<span class="fae-color-locked-text">' + FREE_DEFAULT_COLORS.keyboard + '</span>'
     492          );
     493        } else {
     494          // Remove Pro badge if present
     495          $colorLabel.find('.fae-pro-badge-inline').remove();
     496          // Restore color input with user's SAVED color (not current hidden input value)
     497          $colorInput.removeClass('fae-color-locked');
     498          const restoreColor = this.userSelectedColors.keyboard || FREE_DEFAULT_COLORS.keyboard;
     499          $colorInput.html(
     500            '<input type="color" class="fae-color-picker-inline" name="fae_keyboard_options[color]" value="' + restoreColor + '" id="fae-keyboard-color">' +
     501            '<input type="text" value="' + restoreColor + '" id="fae-keyboard-color-text">'
     502          );
     503          // Re-bind color change events
     504          this.bindColorEvents();
     505        }
     506      }
     507    },
     508
     509    handleParticleEffectChange: function () {
     510      const effect = $(this).val();
     511      const effectSettings = $("#particle-effects-tab .fae-effect-settings");
     512      const $appearanceSection = $('#particle-effects-tab .fae-appearance-section');
     513
     514      // Show/hide settings and appearance based on effect
     515      if (effect === "none") {
     516        effectSettings.hide();
     517        $appearanceSection.hide();
     518        $('#particle-effects-tab .fae-hide-cursor-toggle-wrapper').hide();
     519      } else {
     520        effectSettings.show();
     521        $appearanceSection.show();
     522       
     523        // Get effect config for showing/hiding settings
     524        const effectConfig = typeof FAE_PARTICLE_EFFECTS_CONFIG !== 'undefined' && FAE_PARTICLE_EFFECTS_CONFIG[effect]
     525          ? FAE_PARTICLE_EFFECTS_CONFIG[effect]
     526          : {};
     527       
     528        // Show/hide toggle in header based on config
     529        const showHideCursor = effectConfig.supportsHideCursor !== false;
     530        $('#particle-effects-tab .fae-hide-cursor-toggle-wrapper').toggle(showHideCursor);
     531       
     532        // Show/hide color setting based on effect config (supportsColor)
     533        const showColor = effectConfig.supportsColor !== false;
     534        $('#particle-effects-tab .fae-particle-color-setting').toggle(showColor);
     535       
     536        // Show/hide speed setting based on effect config (supportsSpeed)
     537        const showSpeed = effectConfig.supportsSpeed !== false;
     538        $('#particle-effects-tab .fae-particle-speed-setting').toggle(showSpeed);
     539       
     540        // Show/hide Interactive Cursor option based on effect config
     541        const showInteractiveCursor = effectConfig.supportsInteractiveCursor === true;
     542        $('#particle-effects-tab .fae-interactive-cursor-setting').toggle(showInteractiveCursor);
     543      }
     544
     545      // Visually mark selected option within its grid (purple state)
     546      const $option = $(this).closest(".fae-effect-option");
     547      if ($option.length) {
     548        const $grid = $option.closest(".fae-effect-grid");
     549        if ($grid.length) {
     550          $grid.find(".fae-effect-option").removeClass("fae-effect-selected");
     551        } else {
     552          $("#particle-effects-tab .fae-effect-option").removeClass("fae-effect-selected");
     553        }
     554        $option.addClass("fae-effect-selected");
     555      }
     556
     557      // Update preview iframe
     558      if (typeof FaeAdmin.updatePreviewIframe === 'function') {
     559        FaeAdmin.updatePreviewIframe('particle');
     560      }
     561     
     562      // Update effect type restrictions when effect changes
     563      FaeAdmin.updateEffectTypeRestrictions();
     564     
     565      // Update limited customization UI for particle effects
     566      FaeAdmin.updateParticleLimitedCustomization(effect);
     567    },
     568   
     569    // Update particle effect color/speed UI based on limited customization rules
     570    updateParticleLimitedCustomization: function(effect) {
     571      const hasLimitedCustomization = this.particleEffectHasLimitedCustomization(effect);
     572      const canCustomizeColor = this.canCustomizeParticleColor(effect);
     573      const canCustomizeSpeed = this.canCustomizeParticleSpeed(effect);
     574     
     575      // Update data attributes for CSS
     576      $('#particle-effects-tab .fae-particle-color-setting').attr('data-limited-customization', hasLimitedCustomization ? '1' : '0');
     577      $('#particle-effects-tab .fae-particle-speed-setting').attr('data-limited-customization', hasLimitedCustomization ? '1' : '0');
     578     
     579      // Update color setting
     580      const $colorSetting = $('#particle-effects-tab .fae-particle-color-setting');
     581      const $colorLabel = $colorSetting.find('label');
     582      const $colorInput = $colorSetting.find('.fae-color-input-inline');
     583     
     584      // Update Pro badge in label
     585      if (hasLimitedCustomization && !canCustomizeColor) {
     586        // SAVE user's current color before locking (if not already the default)
     587        const currentColorVal = $('#fae-particle-color').val();
     588        if (currentColorVal && currentColorVal !== FREE_DEFAULT_COLORS.particle) {
     589          this.userSelectedColors.particle = currentColorVal;
     590        }
     591       
     592        // Add Pro badge if not present
     593        if (!$colorLabel.find('.fae-pro-badge-inline').length) {
     594          $colorLabel.append('<span class="fae-pro-badge fae-pro-badge-inline">PRO</span>');
     595        }
     596        // Replace color input with locked swatch
     597        $colorInput.addClass('fae-color-locked');
     598        $colorInput.html(
     599          '<div class="fae-color-swatch-locked" style="background-color: ' + FREE_DEFAULT_COLORS.particle + ';" title="Upgrade to Pro to customize color"></div>' +
     600          '<input type="hidden" name="fae_particle_options[color]" value="' + FREE_DEFAULT_COLORS.particle + '" id="fae-particle-color">' +
     601          '<span class="fae-color-locked-text">' + FREE_DEFAULT_COLORS.particle + '</span>'
     602        );
     603      } else {
     604        // Remove Pro badge if present
     605        $colorLabel.find('.fae-pro-badge-inline').remove();
     606        // Restore color input with user's SAVED color (not current hidden input value)
     607        $colorInput.removeClass('fae-color-locked');
     608        const restoreColor = this.userSelectedColors.particle || FREE_DEFAULT_COLORS.particle;
     609        $colorInput.html(
     610          '<input type="color" class="fae-color-picker-inline" name="fae_particle_options[color]" value="' + restoreColor + '" id="fae-particle-color">' +
     611          '<input type="text" value="' + restoreColor + '" id="fae-particle-color-text">'
     612        );
     613        // Re-bind color change events
     614        this.bindColorEvents();
     615      }
     616     
     617      // Update speed setting
     618      const $speedSetting = $('#particle-effects-tab .fae-particle-speed-setting');
     619      const $speedSelect = $speedSetting.find('select');
     620     
     621      if (hasLimitedCustomization && !canCustomizeSpeed) {
     622        // SAVE user's current speed before locking
     623        const currentSpeedVal = $speedSelect.val();
     624        if (currentSpeedVal && currentSpeedVal !== 'normal') {
     625          this.userSelectedSpeeds.particle = currentSpeedVal;
     626        }
     627       
     628        // Disable slow and fast options, force normal
     629        $speedSelect.addClass('fae-speed-locked');
     630        $speedSelect.find('option').each(function() {
     631          const val = $(this).val();
     632          if (val === 'slow' || val === 'fast') {
     633            $(this).prop('disabled', true);
     634            if (!$(this).text().includes('(Pro)')) {
     635              $(this).text($(this).text() + ' (Pro)');
     636            }
     637          } else {
     638            $(this).prop('disabled', false);
     639          }
     640        });
     641        // Force to normal
     642        $speedSelect.val('normal');
     643      } else {
     644        // Enable all speed options and RESTORE user's saved speed
     645        $speedSelect.removeClass('fae-speed-locked');
     646        $speedSelect.find('option').each(function() {
     647          $(this).prop('disabled', false);
     648          $(this).text($(this).text().replace(' (Pro)', ''));
     649        });
     650        // Restore user's saved speed
     651        if (this.userSelectedSpeeds.particle) {
     652          $speedSelect.val(this.userSelectedSpeeds.particle);
     653        }
     654      }
    47655    },
    48656
    49657    handleEffectChange: function () {
    50       const effect = $(this).find('input[type="radio"]').val() || $(this).val();
     658      // Support both label click and direct radio change
     659      const $option = $(this).closest(".fae-effect-option").length
     660        ? $(this).closest(".fae-effect-option")
     661        : $(this).parent(".fae-effect-option");
     662      const effect =
     663        $option.find('input[type="radio"]').val() || $(this).val();
     664
     665      // Visually mark selected option within its grid (purple state)
     666      if ($option.length) {
     667        const $grid = $option.closest(".fae-effect-grid");
     668        if ($grid.length) {
     669          $grid.find(".fae-effect-option").removeClass("fae-effect-selected");
     670        } else {
     671          // Fallback: clear across all options if grid not found
     672          $(".fae-effect-option").removeClass("fae-effect-selected");
     673        }
     674        $option.addClass("fae-effect-selected");
     675      }
     676
    51677      FaeAdmin.updateAdvancedSettings(effect);
     678     
     679      // Hide/show entire appearance section in preview when effect is 'none'
     680      const $appearanceSection = $('#cursor-effects-tab .fae-appearance-section');
     681      if (effect === 'none') {
     682        $appearanceSection.hide();
     683      } else {
     684        $appearanceSection.show();
     685        // For flag-effect, color visibility is handled by flag selection logic
     686        // Other settings visibility is handled by updateAdvancedSettings
     687      }
     688     
     689      // Update preview iframe
     690      if (typeof FaeAdmin.updatePreviewIframe === 'function') {
     691        FaeAdmin.updatePreviewIframe('cursor');
     692      }
     693     
     694      // Update effect type restrictions when effect changes
     695      FaeAdmin.updateEffectTypeRestrictions();
     696     
     697      // Update limited customization UI for cursor effects
     698      FaeAdmin.updateCursorLimitedCustomization(effect);
     699     
     700      // Status indicators only update after save (from PHP), not on selection change
     701    },
     702   
     703    // Update cursor effect color/speed UI based on limited customization rules
     704    updateCursorLimitedCustomization: function(effect) {
     705      const hasLimitedCustomization = this.effectHasLimitedCustomization(effect);
     706      const canCustomizeColor = this.canCustomizeColor(effect);
     707      const canCustomizeSpeed = this.canCustomizeSpeed(effect);
     708     
     709      // Update data attributes for CSS
     710      $('#cursor-effects-tab .fae-color-setting').attr('data-limited-customization', hasLimitedCustomization ? '1' : '0');
     711      $('#cursor-effects-tab .fae-speed-setting').attr('data-limited-customization', hasLimitedCustomization ? '1' : '0');
     712     
     713      // Update color setting
     714      const $colorSetting = $('#cursor-effects-tab .fae-color-setting');
     715      const $colorLabel = $colorSetting.find('label');
     716      const $colorInput = $colorSetting.find('.fae-color-input-inline');
     717     
     718      // Update Pro badge in label
     719      if (hasLimitedCustomization && !canCustomizeColor) {
     720        // SAVE user's current color before locking (if not already the default)
     721        const currentColorVal = $('#fae-cursor-color').val();
     722        if (currentColorVal && currentColorVal !== FREE_DEFAULT_COLORS.cursor) {
     723          this.userSelectedColors.cursor = currentColorVal;
     724        }
     725       
     726        // Add Pro badge if not present
     727        if (!$colorLabel.find('.fae-pro-badge-inline').length) {
     728          $colorLabel.append('<span class="fae-pro-badge fae-pro-badge-inline">PRO</span>');
     729        }
     730        // Replace color input with locked swatch
     731        $colorInput.addClass('fae-color-locked');
     732        $colorInput.html(
     733          '<div class="fae-color-swatch-locked" style="background-color: ' + FREE_DEFAULT_COLORS.cursor + ';" title="Upgrade to Pro to customize color"></div>' +
     734          '<input type="hidden" name="fae_cursor_options[color]" value="' + FREE_DEFAULT_COLORS.cursor + '" id="fae-cursor-color">' +
     735          '<span class="fae-color-locked-text">' + FREE_DEFAULT_COLORS.cursor + '</span>'
     736        );
     737      } else {
     738        // Remove Pro badge if present
     739        $colorLabel.find('.fae-pro-badge-inline').remove();
     740        // Restore color input with user's SAVED color (not current hidden input value)
     741        $colorInput.removeClass('fae-color-locked');
     742        const restoreColor = this.userSelectedColors.cursor || FREE_DEFAULT_COLORS.cursor;
     743        $colorInput.html(
     744          '<input type="color" class="fae-color-picker-inline" name="fae_cursor_options[color]" value="' + restoreColor + '" id="fae-cursor-color">' +
     745          '<input type="text" value="' + restoreColor + '" id="fae-cursor-color-text">'
     746        );
     747        // Re-bind color change events
     748        this.bindColorEvents();
     749      }
     750     
     751      // Update speed setting
     752      const $speedSetting = $('#cursor-effects-tab .fae-speed-setting');
     753      const $speedSelect = $speedSetting.find('select');
     754     
     755      if (hasLimitedCustomization && !canCustomizeSpeed) {
     756        // SAVE user's current speed before locking
     757        const currentSpeedVal = $speedSelect.val();
     758        if (currentSpeedVal && currentSpeedVal !== 'normal') {
     759          this.userSelectedSpeeds.cursor = currentSpeedVal;
     760        }
     761       
     762        // Disable slow and fast options, enable only normal
     763        $speedSelect.addClass('fae-speed-locked');
     764        $speedSelect.find('option').each(function() {
     765          const val = $(this).val();
     766          if (val === 'slow' || val === 'fast') {
     767            $(this).prop('disabled', true);
     768            if (!$(this).text().includes('(Pro)')) {
     769              $(this).text($(this).text() + ' (Pro)');
     770            }
     771          } else {
     772            $(this).prop('disabled', false);
     773          }
     774        });
     775        // Force to normal
     776        $speedSelect.val('normal');
     777      } else {
     778        // Enable all speed options and RESTORE user's saved speed
     779        $speedSelect.removeClass('fae-speed-locked');
     780        $speedSelect.find('option').each(function() {
     781          $(this).prop('disabled', false);
     782          $(this).text($(this).text().replace(' (Pro)', ''));
     783        });
     784        // Restore user's saved speed
     785        if (this.userSelectedSpeeds.cursor) {
     786          $speedSelect.val(this.userSelectedSpeeds.cursor);
     787        }
     788      }
     789    },
     790   
     791    // Bind/re-bind color picker events
     792    bindColorEvents: function() {
     793      // Cursor color
     794      $('#fae-cursor-color').off('input change').on('input change', function() {
     795        const color = $(this).val();
     796        $('#fae-cursor-color-text').val(color);
     797        // Track user's color choice
     798        FaeAdmin.userSelectedColors.cursor = color;
     799        FaeAdmin.updatePreviewIframe('cursor');
     800      });
     801     
     802      $('#fae-cursor-color-text').off('input change').on('input change', function() {
     803        let color = $(this).val();
     804        if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
     805          $('#fae-cursor-color').val(color);
     806          // Track user's color choice
     807          FaeAdmin.userSelectedColors.cursor = color;
     808          FaeAdmin.updatePreviewIframe('cursor');
     809        }
     810      });
     811     
     812      // Keyboard color
     813      $('#fae-keyboard-color').off('input change').on('input change', function() {
     814        const color = $(this).val();
     815        $('#fae-keyboard-color-text').val(color);
     816        // Track user's color choice
     817        FaeAdmin.userSelectedColors.keyboard = color;
     818        FaeAdmin.updatePreviewIframe('keyboard');
     819      });
     820     
     821      // Particle color
     822      $('#fae-particle-color').off('input change').on('input change', function() {
     823        const color = $(this).val();
     824        $('#fae-particle-color-text').val(color);
     825        // Track user's color choice
     826        FaeAdmin.userSelectedColors.particle = color;
     827        FaeAdmin.updatePreviewIframe('particle');
     828      });
     829     
     830      // Particle color text input
     831      $('#fae-particle-color-text').off('input change').on('input change', function() {
     832        let color = $(this).val();
     833        if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
     834          $('#fae-particle-color').val(color);
     835          // Track user's color choice
     836          FaeAdmin.userSelectedColors.particle = color;
     837          FaeAdmin.updatePreviewIframe('particle');
     838        }
     839      });
     840     
     841      // Keyboard color text input
     842      $('#fae-keyboard-color-text').off('input change').on('input change', function() {
     843        let color = $(this).val();
     844        if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
     845          $('#fae-keyboard-color').val(color);
     846          // Track user's color choice
     847          FaeAdmin.userSelectedColors.keyboard = color;
     848          FaeAdmin.updatePreviewIframe('keyboard');
     849        }
     850      });
     851    },
     852
     853    handleCheckboxChange: function () {
     854      const setting = $(this).attr("name");
     855      const value = $(this).is(':checked') ? '1' : '0';
     856     
     857      // Update hidden input if it exists
     858      const hiddenInput = $('input[type="hidden"][name="' + setting + '"]');
     859      if (hiddenInput.length) {
     860        hiddenInput.val(value);
     861      }
    52862    },
    53863
    54864    handleSettingChange: function () {
    55       const setting = $(this).attr("name");
    56       const value = $(this).val();
    57 
    58       // Update settings in real-time if needed
    59       FaeAdmin.updatePreviewSetting(setting, value);
     865      // Settings change handler
    60866    },
    61867
    62868    handleColorChange: function () {
    63869      const color = $(this).val();
    64       const setting = $(this).data("setting");
    65870
    66871      // Update the text input if it exists
     
    76881        stroke: color,
    77882      });
    78 
    79       FaeAdmin.updatePreviewSetting(setting, color);
    80883    },
    81884
    82885    handleFormSubmit: function (e) {
    83886      e.preventDefault();
    84 
    85       // Show loading state
    86       const submitBtn = $(this).find('input[type="submit"]');
    87       const originalText = submitBtn.val();
    88       submitBtn.val("Saving...").prop("disabled", true);
    89 
    90       // Submit the form
    91       this.submit();
    92 
    93       // Reset button after a delay
    94       setTimeout(() => {
    95         submitBtn.val(originalText).prop("disabled", false);
    96       }, 2000);
     887     
     888      const $form = $(this);
     889      const formId = $form.attr('id');
     890     
     891      // Determine which form type and action
     892      let action, nonce, formData, effectType;
     893     
     894      if (formId === 'fae-cursor-form') {
     895        action = 'fae_save_cursor_settings';
     896        effectType = 'cursor';
     897        nonce = typeof faeAdminData !== 'undefined' && faeAdminData.nonces ? faeAdminData.nonces.cursor : '';
     898        formData = $form.serialize();
     899      } else if (formId === 'fae-keyboard-form') {
     900        action = 'fae_save_keyboard_settings';
     901        effectType = 'keyboard';
     902        nonce = typeof faeAdminData !== 'undefined' && faeAdminData.nonces ? faeAdminData.nonces.keyboard : '';
     903       
     904        // Security: Force multi_color to always be 0 (Pro feature protection)
     905        // Even if someone inspects and removes disabled attribute, this ensures it's always 0
     906        // 1. Remove name attribute from checkbox so it won't be submitted
     907        // 2. Hidden input with same name will ensure value is always 0
     908        $form.find('#fae-keyboard-multi-color').removeAttr('name').prop('disabled', true).prop('checked', false);
     909       
     910        formData = $form.serialize();
     911      } else if (formId === 'fae-particle-form') {
     912        action = 'fae_save_particle_settings';
     913        effectType = 'particle';
     914        nonce = typeof faeAdminData !== 'undefined' && faeAdminData.nonces ? faeAdminData.nonces.particle : '';
     915        formData = $form.serialize();
     916      } else {
     917        return;
     918      }
     919     
     920      // Show loading state first (before validation check)
     921      const submitBtn = $form.find('button[type="submit"], input[type="submit"]');
     922      const originalHtml = submitBtn.html();
     923      const originalText = submitBtn.text() || submitBtn.val();
     924      // Store original HTML for restoration
     925      submitBtn.data('original-html', originalHtml);
     926      submitBtn.data('original-text', originalText);
     927      submitBtn.prop("disabled", true);
     928      submitBtn.html('<span style="display: inline-flex; align-items: center; gap: 6px;"><svg class="fae-icon" viewBox="0 0 24 24" style="width: 16px; height: 16px; animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" opacity="0.25"/><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg>Saving...</span>');
     929     
     930      // Check if trying to save a non-none effect while another effect type is active
     931      // Allow preview but prevent saving
     932      const selectedEffect = $form.find('input[name*="[effect]"]:checked').val() || 'none';
     933      if (selectedEffect !== 'none' && FaeAdmin.isAnotherEffectTypeActive(effectType)) {
     934        // Show upgrade modal and prevent form submission
     935        FaeAdmin.showUpgradeNoticeModal();
     936        // Reset button state
     937        submitBtn.prop("disabled", false);
     938        submitBtn.html(originalHtml);
     939        return false;
     940      }
     941     
     942      // Prepare AJAX data
     943      const ajaxData = formData + '&action=' + action + '&nonce=' + nonce;
     944
     945      // Make AJAX request
     946      $.ajax({
     947        url: typeof faeAdminData !== 'undefined' ? faeAdminData.ajaxUrl : ajaxurl,
     948        type: 'POST',
     949        data: ajaxData,
     950        success: function(response) {
     951          if (response.success) {
     952            FaeAdmin.showNotice(response.data.message || 'Settings saved successfully!', 'success');
     953           
     954            // Determine which tab was saved and update only that tab's status indicator
     955            let savedType = null;
     956            if (formId === 'fae-cursor-form') {
     957              savedType = 'cursor';
     958            } else if (formId === 'fae-keyboard-form') {
     959              savedType = 'keyboard';
     960            } else if (formId === 'fae-particle-form') {
     961              savedType = 'particle';
     962            }
     963           
     964            // Update only the saved tab's status indicator using saved options from response
     965            if (savedType && response.data && response.data.options) {
     966              FaeAdmin.updateTabStatusIndicator(savedType, response.data.options);
     967              // Update active effect badges for the saved tab only
     968              FaeAdmin.updateActiveEffectBadges(savedType, response.data.options);
     969              // Update saved effects state
     970              if (response.data.options.effect) {
     971                FaeAdmin.savedEffects[savedType] = response.data.options.effect;
     972              } else {
     973                FaeAdmin.savedEffects[savedType] = 'none';
     974              }
     975            }
     976           
     977            // Update stats after successful save
     978            FaeAdmin.updateStats();
     979            // Update status indicators (legacy)
     980            FaeAdmin.updateStatusIndicators();
     981            // Update effect type restrictions after save
     982            FaeAdmin.updateEffectTypeRestrictions();
     983          } else {
     984            FaeAdmin.showNotice(response.data && response.data.message ? response.data.message : 'Failed to save settings. Please try again.', 'error');
     985          }
     986        },
     987        error: function(xhr, status, error) {
     988          let errorMessage = 'An error occurred while saving settings. Please try again.';
     989          let showUpgradeNotice = false;
     990         
     991          // Check if response contains error data with upgrade notice
     992          if (xhr.responseJSON && xhr.responseJSON.data) {
     993            if (xhr.responseJSON.data.message) {
     994              errorMessage = xhr.responseJSON.data.message;
     995            }
     996            if (xhr.responseJSON.data.upgrade_notice) {
     997              showUpgradeNotice = true;
     998            }
     999          }
     1000         
     1001          FaeAdmin.showNotice(errorMessage, 'error');
     1002         
     1003          // Show upgrade notice modal if needed
     1004          if (showUpgradeNotice) {
     1005            FaeAdmin.showUpgradeNoticeModal();
     1006          }
     1007         
     1008          console.error('AJAX Error:', error);
     1009        },
     1010        complete: function() {
     1011          // Reset button state
     1012          submitBtn.prop("disabled", false);
     1013          submitBtn.html(originalHtml);
     1014        }
     1015      });
    971016    },
    981017
    991018    updateAdvancedSettings: function (effect) {
    100       const effectSettings = $(".fae-effect-settings");
     1019      const effectSettings = $("#cursor-effects-tab .fae-effect-settings");
    1011020
    1021021      // Show/hide settings based on effect
    1031022      if (effect === "none") {
    1041023        effectSettings.hide();
     1024        // Hide cursor appearance subgroup
     1025        $('#fae-cursor-appearance-subgroup').hide();
     1026        // Trigger height update
     1027        $(document).trigger('faeSettingsUpdated');
    1051028      } else {
    1061029        effectSettings.show();
    1071030
    108         // Show effect-specific sections
     1031        // Get effect config
     1032        const effectConfig = typeof getEffectConfig !== 'undefined'
     1033          ? getEffectConfig(effect)
     1034          : {};
     1035
     1036        // Show/hide cursor appearance subgroup based on config
     1037        const showHideCursor = effectConfig.supportsHideCursor !== false;
     1038        const $hideCursorSubgroup = $('#fae-cursor-appearance-subgroup');
     1039        if (showHideCursor) {
     1040          $hideCursorSubgroup.show();
     1041        } else {
     1042          $hideCursorSubgroup.hide();
     1043        }
     1044
     1045        // Show/hide icon settings wrapper based on effect config
     1046        const showIcon = effectConfig.effectType === 'icon';
     1047        if (showIcon) {
     1048          $('.fae-icon-settings-wrapper').show();
     1049        } else {
     1050          $('.fae-icon-settings-wrapper').hide();
     1051        }
     1052       
     1053        // Show/hide icon settings (Size and Icon) based on effect config
     1054        // Size should show for effects that support size (drop-effect, rise-effect, flag-effect)
     1055        // Icon should show for effects that support icon (drop-effect, rise-effect)
     1056        const showSize = effectConfig.supportsSize === true;
     1057        const showIconSetting = effectConfig.supportsIcon === true;
     1058       
     1059        $('.fae-icon-setting').each(function() {
     1060          const $setting = $(this);
     1061          // Check if this is the Size setting or Icon setting
     1062          const isSizeSetting = $setting.find('select[name*="[size]"]').length > 0;
     1063          const isIconSetting = $setting.find('input[name*="[icon]"]').length > 0 || $setting.find('.fae-icon-picker-inline').length > 0;
     1064         
     1065          if (isSizeSetting) {
     1066            // Size setting: show if effect supports size
     1067            $setting.toggle(showSize);
     1068          } else if (isIconSetting) {
     1069            // Icon setting: show if effect supports icon
     1070            $setting.toggle(showIconSetting);
     1071          }
     1072        });
     1073
     1074        // Show/hide multi-color setting for bubbles-effect and magic-trail
     1075        if (effect === 'bubbles-effect' || effect === 'magic-trail') {
     1076          $('.fae-multi-color-setting').show();
     1077        } else {
     1078          $('.fae-multi-color-setting').hide();
     1079        }
     1080
     1081        // Show/hide flag setting for flag-effect
     1082        if (effect === 'flag-effect') {
     1083          $('.fae-flag-setting').show();
     1084          // For flag-effect, handle color and flag position based on flag selection
     1085          const selectedFlag = $('#fae-cursor-flag').val();
     1086          if (selectedFlag) {
     1087            $('.fae-color-setting').hide();
     1088            $('.fae-flag-position-setting').show();
     1089        } else {
     1090            $('.fae-color-setting').show();
     1091            $('.fae-flag-position-setting').hide();
     1092          }
     1093        } else {
     1094          // Hide all flag-related settings for non-flag effects
     1095          $('.fae-flag-setting').hide();
     1096          $('.fae-flag-position-setting').hide();
     1097          // Show color setting if effect supports color
     1098          if (effectConfig.supportsColor !== false) {
     1099            $('.fae-color-setting').show();
     1100          } else {
     1101            $('.fae-color-setting').hide();
     1102          }
     1103        }
     1104
     1105        // Show/hide settings based on config
    1091106        $(".fae-settings-section").each(function () {
    1101107          const $section = $(this);
    111           const attr = $section.data("effect");
    112 
    113           if (!attr) {
    114             // Global settings - always show
     1108          const effectType = $section.data("effect-type");
     1109
     1110          if (!effectType) {
     1111            // Global settings - show/hide based on config
     1112            const showColor = effectConfig.supportsColor !== false;
     1113            const showSpeed = effectConfig.supportsSpeed !== false;
     1114           
     1115            // Show color setting if supported
     1116            $section.find('.fae-setting-group').each(function() {
     1117              const $group = $(this);
     1118              if ($group.find('input[name*="[color]"]').length && !showColor) {
     1119                $group.hide();
     1120              } else if ($group.find('input[name*="[color]"]').length && showColor) {
     1121                $group.show();
     1122              }
     1123              if ($group.find('select[name*="[speed]"]').length && !showSpeed) {
     1124                $group.hide();
     1125              } else if ($group.find('select[name*="[speed]"]').length && showSpeed) {
     1126                $group.show();
     1127              }
     1128            });
    1151129            $section.show();
    1161130            return;
    1171131          }
    1181132
    119           const list = String(attr)
    120             .split(",")
    121             .map((s) => s.trim());
    122           if (list.includes(effect)) {
     1133          // Show section if effect type matches
     1134          if (effectType === effectConfig.effectType) {
    1231135            $section.show();
    1241136          } else {
     
    1261138          }
    1271139        });
    128       }
    129     },
    130 
    131     updatePreviewSetting: function (setting, value) {
    132       // Update settings in real-time if needed
    133       switch (setting) {
    134         case "fae_cursor_options[icon]":
    135           // Update the global icon setting
    136           if (window.faeCursorSettings) {
    137             window.faeCursorSettings.icon = value;
    138           }
    139           break;
    140       }
    141     },
    142 
    143     resetSettings: function () {
    144       if (
    145         confirm(
    146           "Are you sure you want to reset all settings to default values?"
    147         )
    148       ) {
    149         // Set default values
    150         $('input[name="fae_cursor_options[effect]"][value="drop-effect"]').prop(
    151           "checked",
    152           true
    153         );
    154         $('input[name="fae_cursor_options[color]"]').val("#fcba03");
    155         $('input[name="fae_cursor_options[size]"]').val("1.5rem");
    156         $('input[name="fae_cursor_options[speed]"]').val("fast");
    157         $("#fae-selected-icon-input").val("star.svg");
    158 
    159         // Update the icon picker display
    160         const selectedIcon = $("#fae-icon-picker-trigger .fae-selected-icon");
    161         const iconNameSpan = $("#fae-icon-picker-trigger .fae-icon-name");
    162 
    163         // Update icon display to star
    164         selectedIcon.html(
    165           '<svg viewBox="0 0 512 512"><path d="M394,480a16,16,0,0,1-9.39-3L256,383.76,127.39,477a16,16,0,0,1-24.55-18.08L153,310.35,23,221.2A16,16,0,0,1,32,192H192.38l48.4-148.95a16,16,0,0,1,30.44,0l48.4,149H480a16,16,0,0,1,9.05,29.2L359,310.35l50.13,148.53A16,16,0,0,1,394,480Z"/></svg>'
    166         );
    167         iconNameSpan.text("star");
    168 
    169         // Apply color to the selected icon
    170         selectedIcon.find("svg path").css({
    171           fill: "#fcba03",
    172           stroke: "#fcba03",
     1140       
     1141        // Also handle new grouped layout setting items
     1142        $("#cursor-effects-tab .fae-setting-item").each(function() {
     1143          const $item = $(this);
     1144          const hasColor = $item.find('input[name*="[color]"]').length > 0;
     1145          const hasSpeed = $item.find('select[name*="[speed]"]').length > 0;
     1146         
     1147          if (hasColor) {
     1148            const showColor = effectConfig.supportsColor !== false;
     1149            $item.toggle(showColor);
     1150          }
     1151          if (hasSpeed) {
     1152            const showSpeed = effectConfig.supportsSpeed !== false;
     1153            $item.toggle(showSpeed);
     1154          }
    1731155        });
    174 
    175         // Update selection in grid
    176         $(".fae-icon-option").removeClass("selected");
    177         $('.fae-icon-option[data-icon="star.svg"]').addClass("selected");
    178 
    179         // Update settings sections
    180         FaeAdmin.updateAdvancedSettings("drop-effect");
    181 
    182         // Show success message
    183         FaeAdmin.showNotice(
    184           "Settings reset to default values: Drop Effect, Color #fcba03, Fast Speed, Star Icon",
    185           "success"
    186         );
    187       }
     1156       
     1157        // Handle inline settings in appearance section
     1158        $(".fae-inline-setting").each(function() {
     1159          const $setting = $(this);
     1160         
     1161          // Skip flag-specific settings (handled separately above)
     1162          if ($setting.hasClass('fae-flag-setting') || $setting.hasClass('fae-flag-position-setting')) {
     1163            return;
     1164          }
     1165         
     1166          // Skip color setting for all effects (handled separately above based on effect and flag selection)
     1167          if ($setting.hasClass('fae-color-setting')) {
     1168            return;
     1169          }
     1170         
     1171          // Skip multi-color setting (handled separately above)
     1172          if ($setting.hasClass('fae-multi-color-setting')) {
     1173            return;
     1174          }
     1175         
     1176          // Skip icon settings (Size and Icon - handled separately above)
     1177          if ($setting.hasClass('fae-icon-setting')) {
     1178            return;
     1179          }
     1180         
     1181          const hasColor = $setting.find('input[name*="[color]"]').length > 0;
     1182          const hasSpeed = $setting.find('select[name*="[speed]"]').length > 0;
     1183         
     1184          if (hasColor) {
     1185            const showColor = effectConfig.supportsColor !== false;
     1186            $setting.toggle(showColor);
     1187          }
     1188          if (hasSpeed) {
     1189            const showSpeed = effectConfig.supportsSpeed !== false;
     1190            $setting.toggle(showSpeed);
     1191          }
     1192        });
     1193      }
     1194     
     1195      // Trigger height update after settings change
     1196      setTimeout(() => {
     1197        $(document).trigger('faeSettingsUpdated');
     1198      }, 150);
    1881199    },
    1891200
    1901201    loadSettings: function () {
    191       // Load saved settings and update UI
     1202      // Initially hide all effect settings and appearance sections on page load
     1203      // They will only be shown when user actually clicks/selects an effect
     1204      $("#cursor-effects-tab .fae-effect-settings").hide();
     1205      $("#keyboard-effects-tab .fae-keyboard-effect-settings").hide();
     1206      $("#keyboard-effects-tab .fae-appearance-section").hide();
     1207      $("#particle-effects-tab .fae-effect-settings").hide();
     1208      $("#particle-effects-tab .fae-appearance-section").hide();
     1209      $('#fae-cursor-appearance-subgroup').hide();
     1210      $('.fae-icon-settings-wrapper').hide();
     1211
     1212      // Load saved cursor effect settings - but don't show them yet
    1921213      const effect = $(
    1931214        'input[name="fae_cursor_options[effect]"]:checked'
    1941215      ).val();
    195       FaeAdmin.updateAdvancedSettings(effect);
     1216      // Don't call updateAdvancedSettings here - wait for user to click
     1217      FaeAdmin.updateStatusIndicators();
     1218
     1219      // Note: Visual "selected" state (purple highlight) is not applied on initial load
     1220      // It will be applied when user clicks on an effect
     1221      // Settings will also be shown when user clicks on an effect
     1222
     1223      // Load saved keyboard effect settings - but don't show them yet
     1224      const keyboardEffect = $(
     1225        'input[name="fae_keyboard_options[effect]"]:checked'
     1226      ).val();
     1227      // Don't show settings here - wait for user to click
     1228      // Note: Visual "selected" state is not applied on initial load
     1229      // It will be applied when user clicks on an effect
     1230
     1231      // Load saved particle effect settings
     1232      const particleEffect = $(
     1233        'input[name="fae_particle_options[effect]"]:checked'
     1234      ).val();
     1235     
     1236      // Initialize particle appearance settings visibility on page load
     1237      if (particleEffect && particleEffect !== 'none') {
     1238        // Show effect settings and appearance section for saved effect
     1239        $("#particle-effects-tab .fae-effect-settings").show();
     1240        $("#particle-effects-tab .fae-appearance-section").show();
     1241       
     1242        // Get effect config to show/hide color, speed, and interactive cursor settings
     1243        const effectConfig = typeof FAE_PARTICLE_EFFECTS_CONFIG !== 'undefined' && FAE_PARTICLE_EFFECTS_CONFIG[particleEffect]
     1244          ? FAE_PARTICLE_EFFECTS_CONFIG[particleEffect]
     1245          : {};
     1246       
     1247        // Show/hide color setting based on effect config
     1248        const showColor = effectConfig.supportsColor !== false;
     1249        $('#particle-effects-tab .fae-particle-color-setting').toggle(showColor);
     1250       
     1251        // Show/hide speed setting based on effect config
     1252        const showSpeed = effectConfig.supportsSpeed !== false;
     1253        $('#particle-effects-tab .fae-particle-speed-setting').toggle(showSpeed);
     1254       
     1255        // Show/hide Interactive Cursor option based on effect config
     1256        const showInteractiveCursor = effectConfig.supportsInteractiveCursor === true;
     1257        $('#particle-effects-tab .fae-interactive-cursor-setting').toggle(showInteractiveCursor);
     1258       
     1259        // Update limited customization UI
     1260        FaeAdmin.updateParticleLimitedCustomization(particleEffect);
     1261      }
    1961262
    1971263      // Update the selected icon display
    198       if ($(".fae-color-picker").val()) {
    199         const color = $(".fae-color-picker").val();
     1264      if ($("#cursor-effects-tab .fae-color-picker").val()) {
     1265        const color = $("#cursor-effects-tab .fae-color-picker").val();
    2001266        const selectedIcon = $("#fae-icon-picker-trigger .fae-selected-icon");
    2011267        selectedIcon.find("svg path").css({
     
    2041270        });
    2051271      }
     1272     
     1273      // Update all tab status indicators on page load
     1274      FaeAdmin.updateAllTabStatusIndicators();
     1275     
     1276      // Update active effect badges based on saved state (not form selections)
     1277      FaeAdmin.updateActiveEffectBadges();
     1278     
     1279      // Update effect type restrictions on page load
     1280      FaeAdmin.updateEffectTypeRestrictions();
    2061281    },
    2071282
    2081283    updateStats: function () {
    2091284      // Update dashboard statistics
    210       const effect = $(
    211         'input[name="fae_cursor_options[effect]"]:checked'
    212       ).val();
    213       const isActive = effect !== "none";
    214 
    215       $(".fae-stat-value")
    216         .eq(0)
    217         .text(isActive ? "Active" : "Inactive");
    218       $(".fae-stat-value")
    219         .eq(1)
    220         .text(
    221           effect === "none"
    222             ? "None"
    223             : effect.replace("-", " ").replace(/\b\w/g, (l) => l.toUpperCase())
     1285      // Use saved effects state (what's actually saved), not form selections
     1286      const cursorEffect = FaeAdmin.savedEffects.cursor || 'none';
     1287      const keyboardEffect = FaeAdmin.savedEffects.keyboard || 'none';
     1288      const particleEffect = FaeAdmin.savedEffects.particle || 'none';
     1289     
     1290      // Collect active effects
     1291      let activeEffects = [];
     1292      if (cursorEffect && cursorEffect !== "none") {
     1293        const cursorConfig = typeof FAE_CURSOR_EFFECTS_CONFIG !== 'undefined' && FAE_CURSOR_EFFECTS_CONFIG[cursorEffect]
     1294          ? FAE_CURSOR_EFFECTS_CONFIG[cursorEffect]
     1295          : null;
     1296        const cursorName = cursorConfig && cursorConfig.displayName
     1297          ? cursorConfig.displayName
     1298          : cursorEffect.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
     1299        activeEffects.push(cursorName);
     1300      }
     1301      if (keyboardEffect && keyboardEffect !== "none") {
     1302        // Keyboard effects don't have a JS config, so use formatted name
     1303        activeEffects.push(keyboardEffect.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()));
     1304      }
     1305      if (particleEffect && particleEffect !== "none") {
     1306        const particleConfig = typeof FAE_PARTICLE_EFFECTS_CONFIG !== 'undefined' && FAE_PARTICLE_EFFECTS_CONFIG[particleEffect]
     1307          ? FAE_PARTICLE_EFFECTS_CONFIG[particleEffect]
     1308          : null;
     1309        const particleName = particleConfig && particleConfig.displayName
     1310          ? particleConfig.displayName
     1311          : particleEffect.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
     1312        activeEffects.push(particleName);
     1313      }
     1314     
     1315      const isActive = activeEffects.length > 0;
     1316      const $statCards = $(".fae-stat-card");
     1317      const $statusCard = $statCards.eq(0);      // First card: Status
     1318      const $activeEffectsCard = $statCards.eq(1); // Second card: Active Effects
     1319      // Third card (Effects Available) doesn't need JS updates
     1320     
     1321      // Update Status card
     1322      $statusCard.find(".fae-stat-value").text(isActive ? "Active" : "Inactive");
     1323      $statusCard.toggleClass("fae-stat-card-inactive", !isActive);
     1324     
     1325      // Update Active Effects card
     1326      $activeEffectsCard.toggleClass("fae-stat-card-inactive", !isActive);
     1327      const $effectsValue = $activeEffectsCard.find(".fae-stat-value");
     1328      let $effectsDetail = $activeEffectsCard.find(".fae-stat-detail");
     1329     
     1330      if (activeEffects.length === 0) {
     1331        $effectsValue.text("None").removeClass("fae-stat-value-small");
     1332        $effectsDetail.remove();
     1333      } else if (activeEffects.length === 1) {
     1334        $effectsValue.text(activeEffects[0]).removeClass("fae-stat-value-small");
     1335        $effectsDetail.remove();
     1336      } else {
     1337        $effectsValue.text(activeEffects.length + " Active").addClass("fae-stat-value-small");
     1338        if ($effectsDetail.length === 0) {
     1339          $activeEffectsCard.append('<p class="fae-stat-detail">' + activeEffects.join(", ") + '</p>');
     1340        } else {
     1341          $effectsDetail.text(activeEffects.join(", "));
     1342        }
     1343      }
     1344    },
     1345
     1346    updateStatusIndicators: function () {
     1347      // Status indicators are now based on saved settings only (from PHP)
     1348      // They update only after form submission, not on selection change
     1349      // This function is kept for potential future use but doesn't update UI dynamically
     1350    },
     1351   
     1352    updateAllTabStatusIndicators: function () {
     1353      // Update cursor effect tab status - read from saved options, not form values
     1354      // This prevents showing green dots on tabs with unsaved selections
     1355      const cursorEffect = $('input[name="fae_cursor_options[effect]"]:checked').val() || 'none';
     1356      const $cursorStatus = $('.fae-tab-status[data-effect-type="cursor"]');
     1357      if (cursorEffect !== 'none') {
     1358        $cursorStatus.addClass('active').attr('title', 'Active');
     1359      } else {
     1360        $cursorStatus.removeClass('active').attr('title', 'Inactive');
     1361      }
     1362     
     1363      // Update keyboard effect tab status - read from saved options, not form values
     1364      const keyboardEffect = $('input[name="fae_keyboard_options[effect]"]:checked').val() || 'none';
     1365      const $keyboardStatus = $('.fae-tab-status[data-effect-type="keyboard"]');
     1366      if (keyboardEffect !== 'none') {
     1367        $keyboardStatus.addClass('active').attr('title', 'Active');
     1368      } else {
     1369        $keyboardStatus.removeClass('active').attr('title', 'Inactive');
     1370      }
     1371     
     1372      // Update particle effect tab status - read from saved options, not form values
     1373      const particleEffect = $('input[name="fae_particle_options[effect]"]:checked').val() || 'none';
     1374      const $particleStatus = $('.fae-tab-status[data-effect-type="particle"]');
     1375      if (particleEffect !== 'none') {
     1376        $particleStatus.addClass('active').attr('title', 'Active');
     1377      } else {
     1378        $particleStatus.removeClass('active').attr('title', 'Inactive');
     1379      }
     1380    },
     1381   
     1382    updateTabStatusIndicator: function (type, savedOptions) {
     1383      // Update status indicator for a specific tab using saved options
     1384      // This ensures we only show green dots for actually saved effects
     1385      let effect = 'none';
     1386      if (savedOptions && savedOptions.effect) {
     1387        effect = savedOptions.effect;
     1388      }
     1389     
     1390      const $status = $('.fae-tab-status[data-effect-type="' + type + '"]');
     1391      if (effect !== 'none') {
     1392        $status.addClass('active').attr('title', 'Active');
     1393      } else {
     1394        $status.removeClass('active').attr('title', 'Inactive');
     1395      }
     1396    },
     1397
     1398    // Check if another effect type is currently active (using saved state)
     1399    isAnotherEffectTypeActive: function(currentType) {
     1400      // Use saved effects state (what's actually saved, not form selections)
     1401      const cursorEffect = FaeAdmin.savedEffects.cursor || 'none';
     1402      const keyboardEffect = FaeAdmin.savedEffects.keyboard || 'none';
     1403      const particleEffect = FaeAdmin.savedEffects.particle || 'none';
     1404     
     1405      if (currentType === 'cursor') {
     1406        return (keyboardEffect !== 'none' || particleEffect !== 'none');
     1407      } else if (currentType === 'keyboard') {
     1408        return (cursorEffect !== 'none' || particleEffect !== 'none');
     1409      } else if (currentType === 'particle') {
     1410        return (cursorEffect !== 'none' || keyboardEffect !== 'none');
     1411      }
     1412      return false;
     1413    },
     1414   
     1415    // Update UI to mark other effect types when one is active (for visual indication)
     1416    // But don't disable them - allow preview, only prevent saving
     1417    updateEffectTypeRestrictions: function() {
     1418      // Use saved effects state (what's actually saved, not form selections)
     1419      const cursorEffect = FaeAdmin.savedEffects.cursor || 'none';
     1420      const keyboardEffect = FaeAdmin.savedEffects.keyboard || 'none';
     1421      const particleEffect = FaeAdmin.savedEffects.particle || 'none';
     1422     
     1423      // Check which effect types are active
     1424      const cursorActive = cursorEffect !== 'none';
     1425      const keyboardActive = keyboardEffect !== 'none';
     1426      const particleActive = particleEffect !== 'none';
     1427     
     1428      // Mark keyboard and particle effects if cursor is active (for visual indication only)
     1429      // Don't disable - allow preview, only prevent saving
     1430      if (cursorActive) {
     1431        $('#keyboard-effects-tab .fae-effect-option:not([data-effect-id="none"])').addClass('fae-effect-type-disabled');
     1432        $('#particle-effects-tab .fae-effect-option:not([data-effect-id="none"])').addClass('fae-effect-type-disabled');
     1433        // Don't disable radio buttons - allow selection for preview
     1434      } else {
     1435        $('#keyboard-effects-tab .fae-effect-option').removeClass('fae-effect-type-disabled');
     1436        $('#particle-effects-tab .fae-effect-option').removeClass('fae-effect-type-disabled');
     1437      }
     1438     
     1439      // Mark cursor and particle effects if keyboard is active (for visual indication only)
     1440      if (keyboardActive) {
     1441        $('#cursor-effects-tab .fae-effect-option:not([data-effect-id="none"])').addClass('fae-effect-type-disabled');
     1442        $('#particle-effects-tab .fae-effect-option:not([data-effect-id="none"])').addClass('fae-effect-type-disabled');
     1443        // Don't disable radio buttons - allow selection for preview
     1444      } else {
     1445        $('#cursor-effects-tab .fae-effect-option').removeClass('fae-effect-type-disabled');
     1446        $('#particle-effects-tab .fae-effect-option').removeClass('fae-effect-type-disabled');
     1447      }
     1448     
     1449      // Mark cursor and keyboard effects if particle is active (for visual indication only)
     1450      if (particleActive) {
     1451        $('#cursor-effects-tab .fae-effect-option:not([data-effect-id="none"])').addClass('fae-effect-type-disabled');
     1452        $('#keyboard-effects-tab .fae-effect-option:not([data-effect-id="none"])').addClass('fae-effect-type-disabled');
     1453        // Don't disable radio buttons - allow selection for preview
     1454      } else {
     1455        $('#cursor-effects-tab .fae-effect-option').removeClass('fae-effect-type-disabled');
     1456        $('#keyboard-effects-tab .fae-effect-option').removeClass('fae-effect-type-disabled');
     1457      }
     1458    },
     1459   
     1460    // Show upgrade notice modal
     1461    showUpgradeNoticeModal: function() {
     1462      const upgradeUrl = typeof faeAdminData !== 'undefined' && faeAdminData.upgradeUrl
     1463        ? faeAdminData.upgradeUrl
     1464        : 'https://faecursor.com/';
     1465     
     1466      // Create modal if it doesn't exist
     1467      if ($('#fae-upgrade-notice-modal').length === 0) {
     1468        const modalHtml = `
     1469          <div id="fae-upgrade-notice-modal" class="fae-upgrade-modal">
     1470            <div class="fae-upgrade-modal-backdrop"></div>
     1471            <div class="fae-upgrade-modal-content">
     1472              <button type="button" class="fae-upgrade-modal-close" aria-label="Close">
     1473                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width: 20px; height: 20px;">
     1474                  <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
     1475                </svg>
     1476              </button>
     1477              <div class="fae-upgrade-modal-body">
     1478                <svg class="fae-upgrade-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
     1479                  <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
     1480                </svg>
     1481                <h3>Pro Feature</h3>
     1482                <p>Only one effect type can be active at a time in the free version. Upgrade to Pro to use multiple effects (Cursor, Keyboard, and Screen) simultaneously.</p>
     1483                <a href="${upgradeUrl}" target="_blank" class="fae-upgrade-button">Upgrade to Pro</a>
     1484              </div>
     1485            </div>
     1486          </div>
     1487        `;
     1488        $('body').append(modalHtml);
     1489       
     1490        // Close modal handlers
     1491        $('#fae-upgrade-notice-modal .fae-upgrade-modal-close, #fae-upgrade-notice-modal .fae-upgrade-modal-backdrop').on('click', function() {
     1492          $('#fae-upgrade-notice-modal').removeClass('active');
     1493        });
     1494       
     1495        // Close on Escape key
     1496        $(document).on('keydown', function(e) {
     1497          if (e.key === 'Escape' && $('#fae-upgrade-notice-modal').hasClass('active')) {
     1498            $('#fae-upgrade-notice-modal').removeClass('active');
     1499          }
     1500        });
     1501      }
     1502     
     1503      // Show modal
     1504      $('#fae-upgrade-notice-modal').addClass('active');
     1505    },
     1506
     1507    updateActiveEffectBadges: function (specificType, savedOptions) {
     1508      // If specificType and savedOptions are provided, update only that tab
     1509      // Otherwise, update all tabs based on saved state
     1510      if (specificType && savedOptions) {
     1511        // Remove badges and classes only for the specific tab being updated
     1512        const $tab = $('#' + specificType + '-effects-tab');
     1513        $tab.find('.fae-effect-active-badge').remove();
     1514        $tab.find('.fae-effect-option').removeClass('fae-effect-active');
     1515       
     1516        // Update only the specific tab that was saved
     1517        const effect = savedOptions.effect || 'none';
     1518        if (effect !== 'none') {
     1519          // Update saved state
     1520          FaeAdmin.savedEffects[specificType] = effect;
     1521         
     1522          // Update badge for this specific tab
     1523          const $option = $tab.find('.fae-effect-option[data-effect-id="' + effect + '"]');
     1524          if ($option.length) {
     1525            $option.addClass('fae-effect-active');
     1526            $option.prepend('<span class="fae-effect-active-badge" title="Currently active on website">Active</span>');
     1527          }
     1528        } else {
     1529          // Effect was disabled
     1530          FaeAdmin.savedEffects[specificType] = 'none';
     1531        }
     1532      } else {
     1533        // Remove all existing active badges and classes (for full refresh)
     1534        $('.fae-effect-active-badge').remove();
     1535        $('.fae-effect-option').removeClass('fae-effect-active');
     1536        // Update all tabs based on saved state (not form values)
     1537        // Update cursor effects (within cursor effects tab)
     1538        const cursorEffect = FaeAdmin.savedEffects.cursor;
     1539        if (cursorEffect && cursorEffect !== 'none') {
     1540          const $cursorOption = $('#cursor-effects-tab .fae-effect-option[data-effect-id="' + cursorEffect + '"]');
     1541          if ($cursorOption.length) {
     1542            $cursorOption.addClass('fae-effect-active');
     1543            $cursorOption.prepend('<span class="fae-effect-active-badge" title="Currently active on website">Active</span>');
     1544          }
     1545        }
     1546       
     1547        // Update keyboard effects (within keyboard effects tab)
     1548        const keyboardEffect = FaeAdmin.savedEffects.keyboard;
     1549        if (keyboardEffect && keyboardEffect !== 'none') {
     1550          const $keyboardOption = $('#keyboard-effects-tab .fae-effect-option[data-effect-id="' + keyboardEffect + '"]');
     1551          if ($keyboardOption.length) {
     1552            $keyboardOption.addClass('fae-effect-active');
     1553            $keyboardOption.prepend('<span class="fae-effect-active-badge" title="Currently active on website">Active</span>');
     1554          }
     1555        }
     1556       
     1557        // Update particle effects (within particle effects tab)
     1558        const particleEffect = FaeAdmin.savedEffects.particle;
     1559        if (particleEffect && particleEffect !== 'none') {
     1560          const $particleOption = $('#particle-effects-tab .fae-effect-option[data-effect-id="' + particleEffect + '"]');
     1561          if ($particleOption.length) {
     1562            $particleOption.addClass('fae-effect-active');
     1563            $particleOption.prepend('<span class="fae-effect-active-badge" title="Currently active on website">Active</span>');
     1564          }
     1565        }
     1566      }
     1567    },
     1568
     1569    showNotice: function (message, type) {
     1570      // Remove any existing notices first with smooth fade out
     1571      $('.fae-ajax-notice').each(function() {
     1572        const $existing = $(this);
     1573        if (!$existing.hasClass('fade-out')) {
     1574          $existing.addClass('fade-out');
     1575          setTimeout(() => {
     1576            $existing.remove();
     1577          }, 350);
     1578        }
     1579      });
     1580     
     1581      // Wait a bit for existing notice to fade out before showing new one
     1582      setTimeout(() => {
     1583        const icon = type === 'success'
     1584          ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width: 20px; height: 20px; flex-shrink: 0;"><path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
     1585          : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width: 20px; height: 20px; flex-shrink: 0;"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/></svg>';
     1586       
     1587        // Calculate top position - account for WordPress admin bar
     1588        const getTopPosition = function() {
     1589          const adminBar = $('#wpadminbar');
     1590          if (adminBar.length && adminBar.is(':visible')) {
     1591            return (adminBar.outerHeight() + 20) + 'px';
     1592          }
     1593          // Check if we're in WordPress admin
     1594          if ($('body').hasClass('wp-admin')) {
     1595            return '32px';
     1596          }
     1597          return '20px';
     1598        };
     1599        const topPosition = getTopPosition();
     1600       
     1601      const notice = $(
     1602          '<div class="fae-ajax-notice fae-ajax-notice-' + type + '">' +
     1603          '<div class="fae-notice-content">' +
     1604          '<span class="fae-notice-icon">' + icon + '</span>' +
     1605          '<span class="fae-notice-message">' + message + '</span>' +
     1606          '<button type="button" class="fae-notice-close" title="Dismiss">' +
     1607          '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width: 18px; height: 18px;"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
     1608          '</button>' +
     1609          '</div>' +
     1610          '</div>'
    2241611        );
    225     },
    226 
    227     showNotice: function (message, type) {
    228       const notice = $(
    229         '<div class="fae-notice fae-notice-' + type + '">' + message + "</div>"
    230       );
    231       $(".fae-cursor-dashboard").prepend(notice);
    232 
     1612       
     1613        // Add animation styles if not already added
     1614        if (!$('#fae-notice-styles').length) {
     1615          $('head').append(
     1616            '<style id="fae-notice-styles">' +
     1617            '.fae-ajax-notice { ' +
     1618            '  position: fixed; ' +
     1619            '  right: 20px; ' +
     1620            '  z-index: 100001; ' +
     1621            '  min-width: 320px; ' +
     1622            '  max-width: 500px; ' +
     1623            '  pointer-events: none; ' +
     1624            '  opacity: 0; ' +
     1625            '  transform: translateX(calc(100% + 20px)); ' +
     1626            '  transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); ' +
     1627            '}' +
     1628            '.fae-ajax-notice.show { ' +
     1629            '  opacity: 1; ' +
     1630            '  transform: translateX(0); ' +
     1631            '  pointer-events: auto; ' +
     1632            '}' +
     1633            '.fae-ajax-notice.fade-out { ' +
     1634            '  opacity: 0; ' +
     1635            '  transform: translateX(calc(100% + 20px)); ' +
     1636            '  pointer-events: none; ' +
     1637            '}' +
     1638            '.fae-notice-content { ' +
     1639            '  background: ' + (type === 'success' ? '#10b981' : '#ef4444') + '; ' +
     1640            '  color: white; ' +
     1641            '  padding: 16px 20px; ' +
     1642            '  border-radius: 12px; ' +
     1643            '  box-shadow: 0 20px 40px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05); ' +
     1644            '  display: flex; ' +
     1645            '  align-items: center; ' +
     1646            '  gap: 14px; ' +
     1647            '  backdrop-filter: blur(10px); ' +
     1648            '}' +
     1649            '.fae-notice-icon { ' +
     1650            '  flex-shrink: 0; ' +
     1651            '  display: flex; ' +
     1652            '  align-items: center; ' +
     1653            '  justify-content: center; ' +
     1654            '}' +
     1655            '.fae-notice-message { ' +
     1656            '  flex: 1; ' +
     1657            '  font-size: 14px; ' +
     1658            '  line-height: 1.5; ' +
     1659            '  font-weight: 500; ' +
     1660            '  letter-spacing: -0.01em; ' +
     1661            '}' +
     1662            '.fae-notice-close { ' +
     1663            '  background: rgba(255,255,255,0.2); ' +
     1664            '  border: none; ' +
     1665            '  color: white; ' +
     1666            '  cursor: pointer; ' +
     1667            '  padding: 6px; ' +
     1668            '  margin-left: 4px; ' +
     1669            '  border-radius: 6px; ' +
     1670            '  display: flex; ' +
     1671            '  align-items: center; ' +
     1672            '  justify-content: center; ' +
     1673            '  opacity: 0.8; ' +
     1674            '  transition: all 0.2s ease; ' +
     1675            '  flex-shrink: 0; ' +
     1676            '}' +
     1677            '.fae-notice-close:hover { ' +
     1678            '  opacity: 1; ' +
     1679            '  background: rgba(255,255,255,0.3); ' +
     1680            '  transform: scale(1.1); ' +
     1681            '}' +
     1682            '.fae-notice-close:active { ' +
     1683            '  transform: scale(0.95); ' +
     1684            '}' +
     1685            '@keyframes spin { ' +
     1686            '  from { transform: rotate(0deg); } ' +
     1687            '  to { transform: rotate(360deg); } ' +
     1688            '}' +
     1689            '@media (max-width: 782px) { ' +
     1690            '  .fae-ajax-notice { ' +
     1691            '    right: 10px; ' +
     1692            '    left: 10px; ' +
     1693            '    min-width: auto; ' +
     1694            '    max-width: none; ' +
     1695            '  }' +
     1696            '}' +
     1697            '</style>'
     1698          );
     1699        }
     1700       
     1701        // Set top position dynamically
     1702        notice.css('top', topPosition);
     1703       
     1704        $('body').append(notice);
     1705       
     1706        // Trigger animation by adding show class after a tiny delay
    2331707      setTimeout(() => {
    234         notice.fadeOut(500, function () {
     1708          notice.addClass('show');
     1709        }, 10);
     1710       
     1711        // Auto-dismiss after 4.5 seconds
     1712        const autoDismiss = setTimeout(() => {
     1713          notice.removeClass('show').addClass('fade-out');
     1714          setTimeout(() => {
    2351715          notice.remove();
     1716          }, 400);
     1717        }, 4500);
     1718       
     1719        // Manual dismiss
     1720        notice.find('.fae-notice-close').on('click', function(e) {
     1721          e.stopPropagation();
     1722          clearTimeout(autoDismiss);
     1723          notice.removeClass('show').addClass('fade-out');
     1724          setTimeout(() => {
     1725            notice.remove();
     1726          }, 400);
    2361727        });
    237       }, 3000);
     1728      }, 100);
    2381729    },
    2391730
     
    3201811      $(".fae-icon-option").removeClass("selected");
    3211812      $this.addClass("selected");
    322 
    323       // Update preview if effect is active
    324       FaeAdmin.updatePreviewSetting("fae_cursor_options[icon]", iconFile);
    325 
    326       // Close modal
    327       FaeAdmin.closeIconPicker(e);
    3281813    },
    3291814
     
    3431828      });
    3441829    },
     1830
     1831    handleScopeTypeChange: function () {
     1832      const $select = $(this);
     1833      const scopeType = $select.val();
     1834      // Look for scope options in multiple possible parent structures (old and new layout)
     1835      let $scopeOptions = $select.closest('.fae-settings-section').find('.fae-scope-option');
     1836      if ($scopeOptions.length === 0) {
     1837        $scopeOptions = $select.closest('.fae-settings-subgroup').find('.fae-scope-option');
     1838      }
     1839      if ($scopeOptions.length === 0) {
     1840        $scopeOptions = $select.closest('.fae-settings-subgroup-content').find('.fae-scope-option');
     1841      }
     1842     
     1843      // Hide all scope options with animation
     1844      $scopeOptions.slideUp(200);
     1845     
     1846      // Show the selected scope option with animation
     1847      $scopeOptions.filter('[data-scope-type="' + scopeType + '"]').slideDown(200);
     1848    },
     1849
     1850    handleUserRestrictionTypeChange: function () {
     1851      const $radio = $(this);
     1852     
     1853      // Don't handle if radio is disabled (Pro feature)
     1854      if ($radio.is(':disabled')) {
     1855        return;
     1856      }
     1857     
     1858      const restrictionType = $radio.val();
     1859      // Look for roles wrapper in multiple possible parent structures (old and new layout)
     1860      let $wrapper = $radio.closest('.fae-setting-group').find('.fae-specific-roles-wrapper');
     1861      if ($wrapper.length === 0) {
     1862        $wrapper = $radio.closest('.fae-settings-subgroup-content').find('.fae-specific-roles-wrapper');
     1863      }
     1864      if ($wrapper.length === 0) {
     1865        $wrapper = $radio.closest('.fae-settings-subgroup').find('.fae-specific-roles-wrapper');
     1866      }
     1867     
     1868      if (restrictionType === 'specific') {
     1869        $wrapper.slideDown(200);
     1870      } else {
     1871        $wrapper.slideUp(200);
     1872        // Uncheck all role checkboxes when switching to "All Users"
     1873        $wrapper.find('input[type="checkbox"][name*="[user_roles]"]').prop('checked', false);
     1874      }
     1875    },
     1876
     1877    initPreviewIframes: function() {
     1878      const adminUrl = typeof faeAdminData !== 'undefined' ? faeAdminData.adminUrl : (window.location.origin + '/wp-admin/admin.php');
     1879     
     1880      // Effects that support icon/size (from config)
     1881      const iconEffects = ['drop-effect', 'rise-effect'];
     1882     
     1883      // Show/hide icon settings based on selected effect
     1884      function updateIconSettingsVisibility(effect) {
     1885        const showIcon = iconEffects.includes(effect);
     1886        $('.fae-icon-setting').toggle(showIcon);
     1887      }
     1888     
     1889      // Debounced iframe update - make it accessible to handlers
     1890      const updateIframe = debounce(function(type) {
     1891        let iframe, effect, color, speed, size, icon, flag, flagPosition, multiColor, bg;
     1892       
     1893        if (type === 'cursor') {
     1894          iframe = document.getElementById('fae-cursor-preview-iframe');
     1895          effect = $('input[name="fae_cursor_options[effect]"]:checked').val() || 'none';
     1896          color = $('#fae-cursor-color').val() || '#667eea';
     1897          speed = $('#fae-cursor-speed').val() || 'normal';
     1898          size = $('#fae-cursor-size').val() || '1.5rem';
     1899          icon = $('#fae-cursor-icon').val() || 'star.svg';
     1900          flag = $('#fae-cursor-flag').val() || '';
     1901          flagPosition = $('#fae-cursor-flag-position').val() || 'center';
     1902          multiColor = $('#fae-cursor-multi-color').is(':checked') ? '1' : '0';
     1903          bg = $('#fae-cursor-preview-bg').attr('data-bg') || 'dark';
     1904         
     1905          // Update icon settings visibility
     1906          updateIconSettingsVisibility(effect);
     1907        } else if (type === 'keyboard') {
     1908          iframe = document.getElementById('fae-keyboard-preview-iframe');
     1909          effect = $('input[name="fae_keyboard_options[effect]"]:checked').val() || 'none';
     1910          // Use sparkle color picker if sparkle-keys is selected, otherwise use regular color picker
     1911          if (effect === 'sparkle-keys') {
     1912            color = $('#fae-keyboard-color-sparkle').val() || $('#fae-keyboard-color').val() || '#667eea';
     1913          } else {
     1914            color = $('#fae-keyboard-color').val() || '#667eea';
     1915          }
     1916          speed = 'normal';
     1917          size = '1.5rem';
     1918          icon = 'star.svg';
     1919          flag = '';
     1920          flagPosition = 'center';
     1921          multiColor = '0'; // Multi-color is disabled (Pro feature)
     1922          bg = $('#fae-keyboard-preview-bg').attr('data-bg') || 'dark';
     1923        } else if (type === 'particle') {
     1924          iframe = document.getElementById('fae-particle-preview-iframe');
     1925          effect = $('input[name="fae_particle_options[effect]"]:checked').val() || 'none';
     1926          color = $('#fae-particle-color').val() || '#667eea';
     1927          speed = $('#fae-particle-speed').val() || 'normal';
     1928          size = '1.5rem';
     1929          icon = 'star.svg';
     1930          flag = '';
     1931          flagPosition = 'center';
     1932          multiColor = '0';
     1933          bg = $('#fae-particle-preview-bg').attr('data-bg') || 'dark';
     1934        }
     1935       
     1936        if (iframe) {
     1937          // Remove loaded class to hide iframe during reload
     1938          iframe.classList.remove('loaded');
     1939         
     1940          const params = new URLSearchParams({
     1941            fae_embed_preview: '1',
     1942            type: type,
     1943            effect: effect,
     1944            color: color,
     1945            speed: speed,
     1946            size: size,
     1947            icon: icon,
     1948            flag: flag || '',
     1949            flag_position: flagPosition || 'center',
     1950            multi_color: multiColor,
     1951            bg: bg || 'dark',
     1952            _t: Date.now() // Cache-busting parameter to ensure iframe reloads
     1953          });
     1954          iframe.src = adminUrl + '?' + params.toString();
     1955         
     1956          // Add loaded class when iframe finishes loading
     1957          iframe.onload = function() {
     1958            this.classList.add('loaded');
     1959          };
     1960        }
     1961      }, 300);
     1962     
     1963      // Make updateIframe accessible to handlers
     1964      FaeAdmin.updatePreviewIframe = updateIframe;
     1965     
     1966      // Cursor effect changes
     1967      $('input[name="fae_cursor_options[effect]"]').on('change', function() {
     1968        const effect = $(this).val();
     1969        // Update all settings visibility based on effect
     1970        FaeAdmin.updateAdvancedSettings(effect);
     1971        // For flag-effect, also update color picker visibility based on flag selection
     1972        if (effect === 'flag-effect') {
     1973          updateColorPickerVisibility();
     1974        }
     1975        updateIframe('cursor');
     1976      });
     1977     
     1978      // Initialize icon settings visibility on page load
     1979      const initialEffect = $('input[name="fae_cursor_options[effect]"]:checked').val() || 'none';
     1980      updateIconSettingsVisibility(initialEffect);
     1981      // Initialize all settings visibility using updateAdvancedSettings
     1982      FaeAdmin.updateAdvancedSettings(initialEffect);
     1983      // For flag-effect, also update color picker visibility based on flag selection
     1984      if (initialEffect === 'flag-effect') {
     1985        updateColorPickerVisibility();
     1986      }
     1987      $('#fae-cursor-color').on('input', function() {
     1988        $('#fae-cursor-color-text').val(this.value);
     1989        updateIframe('cursor');
     1990      });
     1991      $('#fae-cursor-color-text').on('change', function() {
     1992        $('#fae-cursor-color').val(this.value);
     1993        updateIframe('cursor');
     1994      });
     1995      $('#fae-cursor-speed').on('change', function() {
     1996        // Track user's speed choice
     1997        FaeAdmin.userSelectedSpeeds.cursor = $(this).val();
     1998        updateIframe('cursor');
     1999      });
     2000      $('#fae-cursor-size, #fae-cursor-flag-position').on('change', function() {
     2001        updateIframe('cursor');
     2002      });
     2003      $('#fae-cursor-multi-color').on('change', function() {
     2004        updateIframe('cursor');
     2005      });
     2006     
     2007      // Icon picker toggle
     2008      $('#fae-icon-trigger-cursor').on('click', function(e) {
     2009        e.preventDefault();
     2010        $('#fae-icon-dropdown-cursor').toggleClass('active');
     2011      });
     2012     
     2013      // Flag picker toggle
     2014      $('#fae-flag-trigger-cursor').on('click', function(e) {
     2015        e.preventDefault();
     2016        $('#fae-flag-dropdown-cursor').toggleClass('active');
     2017      });
     2018     
     2019      // Flag selection
     2020      $('#fae-flag-dropdown-cursor').on('click', '.fae-flag-dropdown-item', function() {
     2021        const $item = $(this);
     2022        const flagFile = $item.data('flag') || '';
     2023        const flagCode = $item.data('name') || '';
     2024       
     2025        // Update hidden input
     2026        $('#fae-cursor-flag').val(flagFile);
     2027       
     2028        // Update button display
     2029        if (flagFile) {
     2030          const flagUrl = (typeof faeAdminData !== 'undefined' ? faeAdminData.assetsUrl : '') + 'flags/' + flagFile;
     2031          $('#fae-flag-preview-cursor').html('<img src="' + flagUrl + '" alt="' + flagCode + '" style="width: 24px; height: 18px; object-fit: cover; border-radius: 2px; border: 1px solid #e5e7eb;">');
     2032          $('#fae-flag-name-cursor').text(flagCode);
     2033          // Hide color picker and show flag position when flag is selected
     2034          $('.fae-color-setting').hide();
     2035          $('.fae-flag-position-setting').show();
     2036        } else {
     2037          $('#fae-flag-preview-cursor').html('<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 20px; height: 20px; color: #9ca3af;"><path d="M19 11H5m14 0a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2m14 0V9a2 2 0 0 0-2-2M5 11V9a2 2 0 0 1 2-2m0 0V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2M7 7h10"/></svg>');
     2038          $('#fae-flag-name-cursor').text('FILL');
     2039          // Show color picker and hide flag position when FILL is selected
     2040          $('.fae-color-setting').show();
     2041          $('.fae-flag-position-setting').hide();
     2042        }
     2043       
     2044        // Update selected state
     2045        $('#fae-flag-dropdown-cursor .fae-flag-dropdown-item').removeClass('selected');
     2046        $item.addClass('selected');
     2047       
     2048        // Close dropdown
     2049        $('#fae-flag-dropdown-cursor').removeClass('active');
     2050       
     2051        // Update preview
     2052        updateIframe('cursor');
     2053      });
     2054     
     2055      // Show/hide color picker and flag position based on initial flag selection
     2056      function updateColorPickerVisibility() {
     2057        const selectedFlag = $('#fae-cursor-flag').val();
     2058        if ($('input[name="fae_cursor_options[effect]"]:checked').val() === 'flag-effect') {
     2059          if (selectedFlag) {
     2060            $('.fae-color-setting').hide();
     2061            $('.fae-flag-position-setting').show();
     2062          } else {
     2063            $('.fae-color-setting').show();
     2064            $('.fae-flag-position-setting').hide();
     2065          }
     2066        }
     2067      }
     2068     
     2069      // Initialize color picker visibility on page load
     2070      updateColorPickerVisibility();
     2071     
     2072      // Note: Effect change handling is done in the handler above (line 776)
     2073      // This ensures all settings are properly updated via updateAdvancedSettings
     2074     
     2075      // Flag search - search by country name
     2076      $('#fae-flag-search-cursor').on('input', function() {
     2077        const searchTerm = $(this).val().toLowerCase();
     2078        $('#fae-flag-grid-cursor .fae-flag-dropdown-item').each(function() {
     2079          const countryName = $(this).data('country-name') || '';
     2080          if (countryName.includes(searchTerm)) {
     2081            $(this).show();
     2082          } else {
     2083            $(this).hide();
     2084          }
     2085        });
     2086      });
     2087     
     2088      // Close flag dropdown when clicking outside
     2089      $(document).on('click', function(e) {
     2090        if (!$(e.target).closest('.fae-flag-picker-inline').length) {
     2091          $('#fae-flag-dropdown-cursor').removeClass('active');
     2092        }
     2093      });
     2094     
     2095      // Icon selection
     2096      $('#fae-icon-dropdown-cursor').on('click', '.fae-icon-dropdown-item', function() {
     2097        const $item = $(this);
     2098        const iconFile = $item.data('icon');
     2099        const iconSvg = $item.html();
     2100        const iconName = iconFile.replace('.svg', '');
     2101       
     2102        // Update hidden input
     2103        $('#fae-cursor-icon').val(iconFile);
     2104       
     2105        // Update button display
     2106        $('#fae-icon-preview-cursor').html(iconSvg);
     2107        $('#fae-icon-name-cursor').text(iconName);
     2108       
     2109        // Update selected state
     2110        $('#fae-icon-dropdown-cursor .fae-icon-dropdown-item').removeClass('selected');
     2111        $item.addClass('selected');
     2112       
     2113        // Close dropdown
     2114        $('#fae-icon-dropdown-cursor').removeClass('active');
     2115       
     2116        // Update preview
     2117        updateIframe('cursor');
     2118      });
     2119     
     2120      // Close dropdown when clicking outside
     2121      $(document).on('click', function(e) {
     2122        if (!$(e.target).closest('.fae-icon-picker-inline').length) {
     2123          $('.fae-icon-dropdown').removeClass('active');
     2124        }
     2125        if (!$(e.target).closest('.fae-flag-picker-inline').length) {
     2126          $('.fae-flag-dropdown').removeClass('active');
     2127        }
     2128      });
     2129     
     2130      // Keyboard effect changes
     2131      $('input[name="fae_keyboard_options[effect]"]').on('change', function() {
     2132        updateIframe('keyboard');
     2133      });
     2134      $('#fae-keyboard-color, #fae-keyboard-color-sparkle').on('input', function() {
     2135        const $textInput = $(this).attr('id') === 'fae-keyboard-color' ? $('#fae-keyboard-color-text') : $('#fae-keyboard-color-text-sparkle');
     2136        $textInput.val(this.value);
     2137        // Sync both color inputs if sparkle-keys
     2138        if ($('input[name="fae_keyboard_options[effect]"]:checked').val() === 'sparkle-keys') {
     2139          if ($(this).attr('id') === 'fae-keyboard-color') {
     2140            $('#fae-keyboard-color-sparkle').val(this.value);
     2141            $('#fae-keyboard-color-text-sparkle').val(this.value);
     2142          } else {
     2143            $('#fae-keyboard-color').val(this.value);
     2144            $('#fae-keyboard-color-text').val(this.value);
     2145          }
     2146        }
     2147        updateIframe('keyboard');
     2148      });
     2149      $('#fae-keyboard-color-text, #fae-keyboard-color-text-sparkle').on('change', function() {
     2150        const $colorInput = $(this).attr('id') === 'fae-keyboard-color-text' ? $('#fae-keyboard-color') : $('#fae-keyboard-color-sparkle');
     2151        $colorInput.val(this.value);
     2152        // Sync both color inputs if sparkle-keys
     2153        if ($('input[name="fae_keyboard_options[effect]"]:checked').val() === 'sparkle-keys') {
     2154          if ($(this).attr('id') === 'fae-keyboard-color-text') {
     2155            $('#fae-keyboard-color-sparkle').val(this.value);
     2156            $('#fae-keyboard-color-text-sparkle').val(this.value);
     2157          } else {
     2158            $('#fae-keyboard-color').val(this.value);
     2159            $('#fae-keyboard-color-text').val(this.value);
     2160          }
     2161        }
     2162        updateIframe('keyboard');
     2163      });
     2164      // Multi-color is disabled (Pro feature) - prevent interaction
     2165      $('#fae-keyboard-multi-color').on('click', function(e) {
     2166        e.preventDefault();
     2167        return false;
     2168      });
     2169     
     2170      // Particle effect changes
     2171      $('input[name="fae_particle_options[effect]"]').on('change', function() {
     2172        updateIframe('particle');
     2173      });
     2174      $('#fae-particle-color').on('input', function() {
     2175        $('#fae-particle-color-text').val(this.value);
     2176        updateIframe('particle');
     2177      });
     2178      $('#fae-particle-color-text').on('change', function() {
     2179        $('#fae-particle-color').val(this.value);
     2180        updateIframe('particle');
     2181      });
     2182      $('#fae-particle-speed').on('change', function() {
     2183        // Track user's speed choice
     2184        FaeAdmin.userSelectedSpeeds.particle = $(this).val();
     2185        updateIframe('particle');
     2186      });
     2187     
     2188      // Load saved background preference from localStorage (fallback to cookie, then default)
     2189      function getSavedBg() {
     2190        // Try localStorage first
     2191        let savedBg = localStorage.getItem('fae_preview_bg');
     2192        if (!savedBg) {
     2193          // Fallback to reading from existing toggle button data attribute (set by PHP from cookie)
     2194          savedBg = $('.fae-preview-bg-toggle').first().attr('data-bg') || 'dark';
     2195          // Save to localStorage for future use
     2196          localStorage.setItem('fae_preview_bg', savedBg);
     2197        }
     2198        return savedBg || 'dark';
     2199      }
     2200     
     2201      // Set cookie helper function
     2202      function setPreviewBgCookie(bg) {
     2203        const expires = new Date();
     2204        expires.setTime(expires.getTime() + (365 * 24 * 60 * 60 * 1000)); // 1 year
     2205        document.cookie = 'fae_preview_bg=' + bg + ';expires=' + expires.toUTCString() + ';path=/';
     2206      }
     2207     
     2208      // Background color toggle button clicks
     2209      $('.fae-preview-bg-toggle').on('click', function() {
     2210        const $toggle = $(this);
     2211        const currentBg = $toggle.attr('data-bg');
     2212        const newBg = currentBg === 'dark' ? 'light' : 'dark';
     2213       
     2214        // Save to both localStorage and cookie
     2215        localStorage.setItem('fae_preview_bg', newBg);
     2216        setPreviewBgCookie(newBg);
     2217       
     2218        // Apply to all toggles (keep them in sync)
     2219        $('.fae-preview-bg-toggle').attr('data-bg', newBg);
     2220       
     2221        // Update iframe wrapper backgrounds immediately to prevent flash
     2222        const bgGradient = newBg === 'light'
     2223          ? 'linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%)'
     2224          : 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)';
     2225        $('.fae-preview-iframe-wrapper').css('background', bgGradient);
     2226       
     2227        const type = $toggle.attr('id').replace('fae-', '').replace('-preview-bg', '');
     2228        updateIframe(type);
     2229      });
     2230     
     2231      // Fullscreen preview modal
     2232      const $modal = $('#fae-preview-modal');
     2233      const $modalIframe = $('#fae-modal-iframe');
     2234      const $modalHint = $('#fae-modal-hint');
     2235     
     2236      // Hint messages for each type
     2237      const hintMessages = {
     2238        cursor: 'Move mouse around to see the effect',
     2239        keyboard: 'Click inside preview and type to see the effect',
     2240        particle: 'Move mouse around to see the effect'
     2241      };
     2242     
     2243      // Expand button click
     2244      $('.fae-expand-btn').on('click', function() {
     2245        const type = $(this).data('preview-type');
     2246        const $iframe = $('#fae-' + type + '-preview-iframe');
     2247        if ($iframe.length) {
     2248          // Get current iframe src and update modal
     2249          $modalIframe.attr('src', $iframe.attr('src'));
     2250          $modalHint.text(hintMessages[type] || hintMessages.cursor);
     2251          $modal.addClass('active');
     2252          $('body').css('overflow', 'hidden');
     2253        }
     2254      });
     2255     
     2256      // Close modal
     2257      $('#fae-modal-close').on('click', function() {
     2258        $modal.removeClass('active');
     2259        $modalIframe.attr('src', '');
     2260        $('body').css('overflow', '');
     2261      });
     2262     
     2263      // Close on Escape key
     2264      $(document).on('keydown', function(e) {
     2265        if (e.key === 'Escape' && $modal.hasClass('active')) {
     2266          $modal.removeClass('active');
     2267          $modalIframe.attr('src', '');
     2268          $('body').css('overflow', '');
     2269        }
     2270      });
     2271     
     2272      // Close on backdrop click
     2273      $modal.on('click', function(e) {
     2274        if (e.target === this) {
     2275          $modal.removeClass('active');
     2276          $modalIframe.attr('src', '');
     2277          $('body').css('overflow', '');
     2278        }
     2279      });
     2280    },
    3452281  };
    3462282
     
    3482284  $(document).ready(function () {
    3492285    FaeAdmin.init();
     2286   
     2287    // Move WordPress admin notices below the header
     2288    FaeAdmin.moveNoticesBelowHeader();
     2289   
     2290    // Watch for dynamically added notices and move them too
     2291    const noticeObserver = new MutationObserver(function(mutations) {
     2292      FaeAdmin.moveNoticesBelowHeader();
     2293    });
     2294   
     2295    // Observe the body for new notices
     2296    if (document.body) {
     2297      noticeObserver.observe(document.body, {
     2298        childList: true,
     2299        subtree: true
     2300      });
     2301    }
    3502302  });
     2303 
     2304  // Function to move notices below header
     2305  FaeAdmin.moveNoticesBelowHeader = function() {
     2306    const dashboard = $('.fae-cursor-dashboard');
     2307    if (dashboard.length === 0) return;
     2308   
     2309    const header = dashboard.find('.fae-dashboard-header');
     2310    if (header.length === 0) return;
     2311   
     2312    // Find all WordPress notices that are not FaeCursor notices
     2313    const notices = $('.notice:not(.fae-notice), .update-nag, .error:not(.fae-notice), .updated:not(.fae-notice)');
     2314   
     2315    notices.each(function() {
     2316      const $notice = $(this);
     2317      // Only process if notice is not already positioned correctly
     2318      const isAfterHeader = $notice.prevAll('.fae-dashboard-header').length > 0;
     2319      const isInDashboard = $notice.closest('.fae-cursor-dashboard').length > 0;
     2320     
     2321      if (!isAfterHeader || !isInDashboard) {
     2322        // Move notice to appear right after the header
     2323        header.after($notice);
     2324      }
     2325    });
     2326  };
    3512327})(jQuery);
  • faecursor/trunk/faecursor.php

    r3384653 r3454888  
    55/*
    66Plugin Name: FaeCursor
    7 Description: Add stunning and customizable mouse cursor effects to your WordPress site. FaeCursor enhances user engagement with interactive, lightweight, and responsive cursor animations. Perfect for portfolios, creative websites, and business sites seeking to stand out. Try FaeCursor today and give your visitors a memorable experience!
    8 Version: 1.1
     7Description: Lightweight WordPress custom cursor plugin — add cursor, keyboard, and screen effects like trails and sparkles.
     8Version: 1.2
    99Author: FaeCursor Plugin Team
    10 Author URI: https://faecursor.wordpress.com
     10Author URI: https://faecursor.com
    1111License: GPLv2 or later
    1212Text Domain: faecursor
     
    1818if ( ! defined( 'ABSPATH' ) ) {
    1919    exit( 'You are not allowed to access this file directly.' );
     20}
     21
     22/**
     23 * Load conflict handler class
     24 */
     25require_once __DIR__ . '/includes/class-fae-cursor-conflict.php';
     26
     27/**
     28 * Initialize conflict prevention
     29 * This prevents free plugin from loading if Pro is active
     30 */
     31if ( Fae_Cursor_Conflict_Free::init() ) {
     32    return; // Exit early if Pro is active
     33}
     34
     35/**
     36 * Initialize Freemius SDK (WordPress.org compliant)
     37 * Following the same approach as Ultimate Cursor
     38 */
     39if ( ! function_exists( 'faecursor_fs' ) ) {
     40    // Create a helper function for easy SDK access.
     41    function faecursor_fs() {
     42        global $faecursor_fs;
     43
     44        if ( ! isset( $faecursor_fs ) ) {
     45            // Activate multisite network integration.
     46            if ( ! defined( 'WP_FS__PRODUCT_22561_MULTISITE' ) ) {
     47                define( 'WP_FS__PRODUCT_22561_MULTISITE', true );
     48            }
     49
     50            // Include Freemius SDK.
     51            require_once dirname( __FILE__ ) . '/vendor/freemius/start.php';
     52
     53            $faecursor_fs = fs_dynamic_init( array(
     54                'id'                  => '22561',
     55                'slug'                => 'faecursor',
     56                'premium_slug'        => 'faecursor-pro',
     57                'type'                => 'plugin',
     58                'public_key'          => 'pk_22357c6e5eb4d6802700ca1aa120d',
     59                'is_premium'          => false,
     60                'is_premium_only'     => false,
     61                'has_addons'          => false,
     62                'has_paid_plans'      => true,
     63                'is_live'             => true,
     64                'is_org_compliant'    => true,
     65                // No parallel_activation - Pro REPLACES Free (standalone model)
     66                'menu'                => array(
     67                    'slug'        => 'fae_cursor',
     68                    'first-path'  => 'admin.php?page=fae_cursor',
     69                    'support'     => false,
     70                    'contact'     => false,
     71                    'pricing'     => true,
     72                ),
     73            ) );
     74        }
     75
     76        return $faecursor_fs;
     77    }
     78
     79    // Init Freemius.
     80    faecursor_fs();
     81    // Signal that SDK was initiated.
     82    do_action( 'faecursor_fs_loaded' );
    2083}
    2184
     
    3093}
    3194
     95// Include effects configuration
     96require_once FAE_CURSOR_DIR . '/config/fae-cursor-effects-config.php';
     97require_once FAE_CURSOR_DIR . '/config/fae-keyboard-effects-config.php';
     98require_once FAE_CURSOR_DIR . '/config/fae-particle-effects-config.php';
     99
    32100// Custom Debug Constant, intended for developer use.
    33101if ( ! defined( 'FAE_CURSOR_DEBUG' ) ) {
     
    37105// Constants.
    38106define( 'FAE_CURSOR_PLUGIN_NAME', 'faecursor' ); // Updated to match text domain
    39 define( 'FAE_CURSOR_VERSION', '1.1' ); // Update this version as necessary
     107define( 'FAE_CURSOR_VERSION', '1.2' ); // Update this version as necessary
    40108
    41 // Define default options
    42 function fae_cursor_get_default_options() {
    43     return array(
    44         'effect' => 'none',
    45         'color' => '#fcba03',
    46         'size' => '1.5rem',
    47         'speed' => 'fast',
    48         'icon' => 'star.svg'
    49     );
     109// Load plugin classes
     110require_once FAE_CURSOR_DIR . '/includes/class-fae-cursor-loader.php';
     111
     112// Initialize the plugin
     113Fae_Cursor_Loader::init();
     114
     115// Backward compatibility functions
     116if ( ! function_exists( 'fae_cursor_get_default_options' ) ) {
     117    function fae_cursor_get_default_options() {
     118        return Fae_Cursor_Settings::get_default_options();
     119    }
    50120}
    51121
    52 function fae_cursor_enqueue_scripts() {
    53     $options = get_option('fae_cursor_options', fae_cursor_get_default_options());
    54     $effect = isset($options['effect']) ? $options['effect'] : 'none';
    55     $timestamp = time(); // Current timestamp for cache busting
    56 
    57     if ($effect !== 'none') {
    58         $effect_dir = plugin_dir_path(__FILE__) . 'assets/effects/' . $effect;
    59 
    60         foreach (glob($effect_dir . '/*.css') as $css_file) {
    61             $handle = 'fae-cursor-style-' . basename($css_file, '.css');
    62             $css_url = plugin_dir_url(__FILE__) . 'assets/effects/' . $effect . '/' . basename($css_file);
    63             wp_enqueue_style($handle, $css_url, array(), $timestamp);
    64         }
    65 
    66         // Enqueue scripts and localize the first one
    67         $localized = false;
    68         foreach (glob($effect_dir . '/*.js') as $js_file) {
    69             $handle = 'fae-cursor-script-' . basename($js_file, '.js');
    70             $js_url = plugin_dir_url(__FILE__) . 'assets/effects/' . $effect . '/' . basename($js_file);
    71             wp_enqueue_script($handle, $js_url, array('jquery'), $timestamp, true);
    72 
    73             if (! $localized) {
    74                 wp_localize_script($handle, 'faeCursorSettings', array(
    75                     'effect' => $effect,
    76                     'color'  => isset($options['color']) ? $options['color'] : '#667eea',
    77                     'size'   => isset($options['size']) ? $options['size'] : '1.5rem',
    78                     'speed'  => isset($options['speed']) ? $options['speed'] : 'normal',
    79                     'icon'   => isset($options['icon']) ? $options['icon'] : 'star.svg',
    80                     'assetsUrl' => plugin_dir_url(__FILE__) . 'assets/',
    81                 ));
    82                 $localized = true;
    83             }
    84         }
    85     }
    86 }
    87 add_action('wp_enqueue_scripts', 'fae_cursor_enqueue_scripts');
    88 
    89 function fae_cursor_add_admin_menu() {
    90     add_menu_page(
    91         'FaeCursor Settings',
    92         'FaeCursor',
    93         'manage_options',
    94         'fae_cursor',
    95         'fae_cursor_options_page',
    96         plugin_dir_url(__FILE__) . 'assets/icons/icon.ico', // Path to your custom image
    97         100
    98     );
    99 }
    100 add_action('admin_menu', 'fae_cursor_add_admin_menu');
    101 
    102 function fae_cursor_settings_init() {
    103     register_setting(
    104         'fae_cursor', // Option group
    105         'fae_cursor_options', // Option name
    106         array(
    107             'type'              => 'array', // Expected data type
    108             'sanitize_callback' => 'fae_cursor_sanitize_options', // Custom sanitization callback
    109         )
    110     );
    111 
    112     // Add settings section
    113     add_settings_section(
    114         'fae_cursor_section', // Section ID
    115         __('Mouse Effect Settings', 'faecursor'), // Title
    116         null, // Callback (not needed here)
    117         'fae_cursor' // Page
    118     );
    119 
    120     // Add settings field
    121     add_settings_field(
    122         'fae_cursor_effect', // Field ID
    123         __('Mouse Effect', 'faecursor'), // Title
    124         'fae_cursor_effect_render', // Callback to render the field
    125         'fae_cursor', // Page
    126         'fae_cursor_section' // Section
    127     );
    128 }
    129 add_action('admin_init', 'fae_cursor_settings_init');
    130 
    131 function fae_cursor_sanitize_options($input) {
    132     $sanitized = array();
    133 
    134     // Sanitize effect
    135     if (isset($input['effect'])) {
    136         $allowed_effects = array('none', 'drop-effect', 'rise-effect', 'line-effect', 'duo-circle', 'duo-circle-2');
    137         $sanitized['effect'] = in_array($input['effect'], $allowed_effects) ? $input['effect'] : 'none'; // Default to 'none'
    138     }
    139 
    140     // Sanitize color
    141     if (isset($input['color'])) {
    142         $sanitized['color'] = sanitize_hex_color($input['color']) ?: '#fcba03';
    143     }
    144 
    145     // Sanitize size
    146     if (isset($input['size'])) {
    147         $allowed_sizes = array('1rem', '1.5rem', '2rem', '2.5rem');
    148         $sanitized['size'] = in_array($input['size'], $allowed_sizes) ? $input['size'] : '1.5rem';
    149     }
    150 
    151     // Sanitize speed
    152     if (isset($input['speed'])) {
    153         $allowed_speeds = array('slow', 'normal', 'fast');
    154         $sanitized['speed'] = in_array($input['speed'], $allowed_speeds) ? $input['speed'] : 'fast';
    155     }
    156 
    157     // Sanitize icon (must be an existing SVG in assets/ionicons)
    158     if (isset($input['icon'])) {
    159         $icon = basename(sanitize_text_field($input['icon']));
    160         $icon_path = plugin_dir_path(__FILE__) . 'assets/ionicons/' . $icon;
    161         if (substr($icon, -4) === '.svg' && file_exists($icon_path)) {
    162             $sanitized['icon'] = $icon;
    163         } else {
    164             $sanitized['icon'] = 'star.svg';
    165         }
    166     }
    167 
    168     return $sanitized;
     122if ( ! function_exists( 'fae_detectDeviceType' ) ) {
     123    function fae_detectDeviceType() {
     124        return Fae_Cursor_Device::detect();
     125    }
    169126}
    170127
    171 function fae_cursor_enqueue_admin_styles($hook) {
    172     // Only load on FaeCursor admin pages
    173     if ($hook !== 'toplevel_page_fae_cursor') {
    174 
    175         wp_enqueue_style(
    176             'fae-cursor-admin-styles',
    177             plugin_dir_url(__FILE__) . 'assets/css/fae-cursor-general.css',
    178             array(),
    179             '1.1.0'
    180         );
    181        
    182     }
    183    
    184     // Ionicons are embedded as SVG, no external dependencies needed
    185 
    186     wp_enqueue_style(
    187         'fae-cursor-admin-styles',
    188         plugin_dir_url(__FILE__) . 'assets/css/fae-cursor-admin.css',
    189         array(),
    190         '1.1.0'
    191     );
    192    
    193     wp_enqueue_script(
    194         'fae-cursor-admin-script',
    195         plugin_dir_url(__FILE__) . 'assets/js/fae-cursor-admin.js',
    196         array('jquery'),
    197         '1.1.0',
    198         true
    199     );
     128if ( ! function_exists( 'fae_cursor_get_effect_icon_svg' ) ) {
     129    function fae_cursor_get_effect_icon_svg($effect_id) {
     130        return Fae_Cursor_Admin::get_effect_icon_svg($effect_id);
     131    }
    200132}
    201133
    202 add_action('admin_enqueue_scripts', 'fae_cursor_enqueue_admin_styles');
    203 
    204 function fae_cursor_effect_render() {
    205     $options = get_option('fae_cursor_options', fae_cursor_get_default_options());
    206     $selected_effect = isset($options['effect']) ? $options['effect'] : 'none';
    207     // Map effect to preview CSS class suffix
    208     $preview_map = array(
    209         'drop-effect' => 'drop',
    210         'rise-effect' => 'rise',
    211         'line-effect' => 'line',
    212         'duo-circle'  => 'duo',
    213         'none'        => 'none'
    214     );
    215     $preview_class = isset($preview_map[$selected_effect]) ? $preview_map[$selected_effect] : 'none';
    216     ?>
    217     <select name="fae_cursor_options[effect]">
    218         <option value="none" <?php selected($selected_effect, 'none'); ?>>None</option>
    219         <option value="drop-effect" <?php selected($selected_effect, 'drop-effect'); ?>>Drop Effect</option>
    220         <option value="rise-effect" <?php selected($selected_effect, 'rise-effect'); ?>>Rise Effect</option>
    221         <option value="line-effect" <?php selected($selected_effect, 'line-effect'); ?>>Line Effect</option>
    222         <option value="duo-circle" <?php selected($selected_effect, 'duo-circle'); ?>>Duo Circle</option>
    223     </select>
    224     <?php
     134// Legacy function for backward compatibility
     135if ( ! function_exists( 'fae_cursor_options_page' ) ) {
     136    function fae_cursor_options_page() {
     137        Fae_Cursor_Admin::render_options_page();
     138    }
    225139}
    226 
    227 function fae_cursor_options_page() {
    228     $options = get_option('fae_cursor_options', fae_cursor_get_default_options());
    229     $selected_effect = isset($options['effect']) ? $options['effect'] : 'none';
    230     $selected_color = isset($options['color']) ? $options['color'] : '#fcba03';
    231     $selected_size = isset($options['size']) ? $options['size'] : '1.5rem';
    232     $selected_speed = isset($options['speed']) ? $options['speed'] : 'fast';
    233     $selected_icon = isset($options['icon']) ? $options['icon'] : 'star.svg';
    234     ?>
    235     <div class="wrap fae-cursor-dashboard">
    236         <!-- Header Section -->
    237         <div class="fae-dashboard-header">
    238             <div class="fae-header-content">
    239                 <div class="fae-header-main">
    240             <h1 class="fae-dashboard-title">
    241                 <img src="<?php echo plugin_dir_url(__FILE__) . 'assets/icons/icon.ico'; ?>" alt="">
    242                 FaeCursor
    243             </h1>
    244                 </div>
    245                 <div class="fae-header-actions">
    246                     <a href="https://faecursor.wordpress.com/feedback/" target="_blank" class="fae-btn fae-btn-feedback" title="We'd love your thoughts! Click to share feedback.">
    247                         <svg class="fae-icon" viewBox="0 0 512 512">
    248                             <path d="M256,48C141.31,48,48,141.31,48,256s93.31,208,208,208,208-93.31,208-208S370.69,48,256,48Zm0,384c-97,0-176-79-176-176S159,80,256,80s176,79,176,176S353,432,256,432Z"/>
    249                         </svg>
    250                         Submit Feedback
    251                     </a>
    252                 </div>
    253             </div>
    254         </div>
    255 
    256         <!-- Stats Cards -->
    257         <div class="fae-stats-grid">
    258             <div class="fae-stat-card">
    259                 <h3>Status</h3>
    260                 <p class="fae-stat-value"><?php echo $selected_effect !== 'none' ? 'Active' : 'Inactive'; ?></p>
    261             </div>
    262             <div class="fae-stat-card">
    263                 <h3>Current Effect</h3>
    264                 <p class="fae-stat-value"><?php echo $selected_effect === 'none' ? 'None' : ucwords(str_replace('-', ' ', $selected_effect)); ?></p>
    265             </div>
    266             <div class="fae-stat-card">
    267                 <h3>Version</h3>
    268                 <p class="fae-stat-value">1.1</p>
    269             </div>
    270             <div class="fae-stat-card">
    271                 <h3>Effects Available</h3>
    272                 <p class="fae-stat-value">5</p>
    273             </div>
    274         </div>
    275 
    276         <!-- Main Content -->
    277         <div class="fae-main-content">
    278             <!-- Settings Panel -->
    279             <div class="fae-settings-panel">
    280                 <h2>
    281                     <svg class="fae-icon" viewBox="0 0 512 512">
    282                         <path d="M470.39,300l-.47-.38-31.56-24.75a16.11,16.11,0,0,1-6.1-13.33l0-11.56a16,16,0,0,1,6.11-13.22L469.92,212l.47-.38a26.68,26.68,0,0,0,5.9-34.06l-42.71-73.9a1.59,1.59,0,0,1-.13-.22A26.86,26.86,0,0,0,401,92.14l-.35.13L363.55,107.2a15.94,15.94,0,0,1-14.47-1.29q-4.92-3.1-10-5.86a15.94,15.94,0,0,1-8.19-11.82L325.3,48.64l-.12-.72A27.22,27.22,0,0,0,298.76,26H213.24a26.92,26.92,0,0,0-26.45,22.39l-.09.56-5.57,39.67A16,16,0,0,1,173,100.44c-3.42,1.84-6.76,3.79-10,5.82a15.92,15.92,0,0,1-14.43-1.27l-37.13-15-.35-.14a26.87,26.87,0,0,0-32.48,11.34l-.13.22L35.71,177.9A26.71,26.71,0,0,0,41.61,212l.47.38,31.56,24.75a16.11,16.11,0,0,1,6.1,13.33l0,11.56a16,16,0,0,1-6.11,13.22L42.08,300l-.47.38a26.68,26.68,0,0,0-5.9,34.06l42.71,73.9a1.59,1.59,0,0,1,.13.22A26.86,26.86,0,0,0,111,419.86l.35-.13,37.07-14.93a15.94,15.94,0,0,1,14.47,1.29q4.92,3.11,10,5.86a15.94,15.94,0,0,1,8.19,11.82l5.56,39.59.12.72A27.22,27.22,0,0,0,213.24,486h85.52a26.92,26.92,0,0,0,26.45-22.39l.09-.56,5.57-39.67a16,16,0,0,1,8.18-11.82c3.42-1.84,6.76-3.79,10-5.82a15.92,15.92,0,0,1,14.43-1.27l37.13,14.95.35.14a26.85,26.85,0,0,0,32.48-11.34,2.53,2.53,0,0,1,.13-.22l42.71-73.89A26.7,26.7,0,0,0,470.39,300ZM335.91,259.76a80,80,0,1,1-83.66-83.67A80.21,80.21,0,0,1,335.91,259.76Z"/>
    283                     </svg>
    284                     Effect Settings
    285                 </h2>
    286 
    287                 <form id="fae-cursor-form" action="options.php" method="post">
    288                     <?php settings_fields('fae_cursor'); ?>
    289                    
    290                     <!-- Effect Selection -->
    291                     <div class="fae-effect-grid">
    292                         <label class="fae-effect-option">
    293                             <input type="radio" name="fae_cursor_options[effect]" value="none" <?php checked($selected_effect, 'none'); ?>>
    294                             <div class="fae-effect-content">
    295                                 <svg class="fae-effect-icon" viewBox="0 0 512 512">
    296                                     <path d="M256,48C141.31,48,48,141.31,48,256s93.31,208,208,208,208-93.31,208-208S370.69,48,256,48Zm0,384c-97,0-176-79-176-176S159,80,256,80s176,79,176,176S353,432,256,432Z"/>
    297                                 </svg>
    298                                 <span class="fae-effect-name">None</span>
    299                             </div>
    300                         </label>
    301 
    302                         <label class="fae-effect-option">
    303                             <input type="radio" name="fae_cursor_options[effect]" value="drop-effect" <?php checked($selected_effect, 'drop-effect'); ?>>
    304                             <div class="fae-effect-content">
    305                                 <svg class="fae-effect-icon" viewBox="0 0 512 512">
    306                                     <path d="M394,480a16,16,0,0,1-9.39-3L256,383.76,127.39,477a16,16,0,0,1-24.55-18.08L153,310.35,23,221.2A16,16,0,0,1,32,192H192.38l48.4-148.95a16,16,0,0,1,30.44,0l48.4,149H480a16,16,0,0,1,9.05,29.2L359,310.35l50.13,148.53A16,16,0,0,1,394,480Z"/>
    307                                 </svg>
    308                                 <span class="fae-effect-name">Drop Effect</span>
    309                             </div>
    310                         </label>
    311 
    312                         <label class="fae-effect-option">
    313                             <input type="radio" name="fae_cursor_options[effect]" value="rise-effect" <?php checked($selected_effect, 'rise-effect'); ?>>
    314                             <div class="fae-effect-content">
    315                                 <svg class="fae-effect-icon" viewBox="0 0 512 512">
    316                                     <path d="M394,480a16,16,0,0,1-9.39-3L256,383.76,127.39,477a16,16,0,0,1-24.55-18.08L153,310.35,23,221.2A16,16,0,0,1,32,192H192.38l48.4-148.95a16,16,0,0,1,30.44,0l48.4,149H480a16,16,0,0,1,9.05,29.2L359,310.35l50.13,148.53A16,16,0,0,1,394,480Z"/>
    317                                 </svg>
    318                                 <span class="fae-effect-name">Rise Effect</span>
    319                             </div>
    320                         </label>
    321 
    322                         <label class="fae-effect-option">
    323                             <input type="radio" name="fae_cursor_options[effect]" value="line-effect" <?php checked($selected_effect, 'line-effect'); ?>>
    324                             <div class="fae-effect-content">
    325                                 <svg class="fae-effect-icon" viewBox="0 0 512 512">
    326                                     <!-- Main diagonal line with thick fading tail effect -->
    327                                     <path d="M100,100 L400,400" stroke="currentColor" stroke-width="16" stroke-linecap="round" fill="none"/>
    328                                    
    329                                     <!-- Fading trail using multiple lines with decreasing opacity and thickness -->
    330                                     <path d="M120,120 L400,400" stroke="currentColor" stroke-width="12" stroke-linecap="round" fill="none" opacity="0.7"/>
    331                                     <path d="M140,140 L400,400" stroke="currentColor" stroke-width="10" stroke-linecap="round" fill="none" opacity="0.5"/>
    332                                     <path d="M160,160 L400,400" stroke="currentColor" stroke-width="8" stroke-linecap="round" fill="none" opacity="0.3"/>
    333                                     <path d="M180,180 L400,400" stroke="currentColor" stroke-width="6" stroke-linecap="round" fill="none" opacity="0.2"/>
    334                                     <path d="M200,200 L400,400" stroke="currentColor" stroke-width="4" stroke-linecap="round" fill="none" opacity="0.1"/>
    335                                 </svg>
    336                                 <span class="fae-effect-name">Line Effect</span>
    337                             </div>
    338                         </label>
    339 
    340                         <label class="fae-effect-option">
    341                             <input type="radio" name="fae_cursor_options[effect]" value="duo-circle" <?php checked($selected_effect, 'duo-circle'); ?>>
    342                             <div class="fae-effect-content">
    343                                 <svg class="fae-effect-icon" viewBox="0 0 512 512">
    344                                     <path d="M256,48C141.31,48,48,141.31,48,256s93.31,208,208,208,208-93.31,208-208S370.69,48,256,48Zm0,384c-97,0-176-79-176-176S159,80,256,80s176,79,176,176S353,432,256,432Z"/>
    345                                 </svg>
    346                                 <span class="fae-effect-name">Duo Circle</span>
    347                             </div>
    348                         </label>
    349 
    350                         <label class="fae-effect-option">
    351                             <input type="radio" name="fae_cursor_options[effect]" value="duo-circle-2" <?php checked($selected_effect, 'duo-circle-2'); ?>>
    352                             <div class="fae-effect-content">
    353                                  <svg class="fae-effect-icon" viewBox="0 0 512 512">
    354                                     <!-- Outer thick circle -->
    355                                     <circle cx="256" cy="256" r="160" stroke="currentColor" stroke-width="20" fill="none"/>
    356                                     <!-- Inner thin circle -->
    357                                     <circle cx="256" cy="256" r="60" stroke="currentColor" stroke-width="8" fill="none"/>
    358                                  </svg>
    359                                 <span class="fae-effect-name">Duo Circle 2</span>
    360                             </div>
    361                         </label>
    362                     </div>
    363 
    364                     <!-- Effect-Specific Settings -->
    365                     <div class="fae-effect-settings" style="<?php echo $selected_effect === 'none' ? 'display: none;' : ''; ?>">
    366                        
    367                         <!-- Global Settings (for all effects) -->
    368                         <div class="fae-settings-section">
    369                             <h3>
    370                                 <svg class="fae-icon" viewBox="0 0 512 512">
    371                                     <path d="M256,48C141.31,48,48,141.31,48,256s93.31,208,208,208,208-93.31,208-208S370.69,48,256,48Zm0,384c-97,0-176-79-176-176S159,80,256,80s176,79,176,176S353,432,256,432Z"/>
    372                                 </svg>
    373                                 Global Settings
    374                             </h3>
    375                             <div class="fae-settings-grid">
    376                                 <div class="fae-setting-group">
    377                             <label for="fae_cursor_color">Effect Color</label>
    378                             <div class="fae-color-input">
    379                                 <input type="color" class="fae-color-picker" name="fae_cursor_options[color]" value="<?php echo esc_attr($selected_color); ?>" data-setting="fae_cursor_options[color]">
    380                                 <input type="text" name="fae_cursor_options[color]" value="<?php echo esc_attr($selected_color); ?>" placeholder="#667eea">
    381                             </div>
    382                         </div>
    383 
    384                                 <div class="fae-setting-group">
    385                                     <label for="fae_cursor_speed">Animation Speed</label>
    386                                     <select name="fae_cursor_options[speed]">
    387                                         <option value="slow" <?php selected($selected_speed, 'slow'); ?>>Slow</option>
    388                                         <option value="normal" <?php selected($selected_speed, 'normal'); ?>>Normal</option>
    389                                         <option value="fast" <?php selected($selected_speed, 'fast'); ?>>Fast</option>
    390                                     </select>
    391                                 </div>
    392                             </div>
    393                         </div>
    394 
    395                         <!-- Icon-based Effects Settings (Drop & Rise) -->
    396                         <div class="fae-settings-section" data-effect="drop-effect,rise-effect" style="<?php echo !in_array($selected_effect, ['drop-effect', 'rise-effect']) ? 'display: none;' : ''; ?>">
    397                             <h3>
    398                                 <svg class="fae-icon" viewBox="0 0 512 512">
    399                                     <path d="M394,480a16,16,0,0,1-9.39-3L256,383.76,127.39,477a16,16,0,0,1-24.55-18.08L153,310.35,23,221.2A16,16,0,0,1,32,192H192.38l48.4-148.95a16,16,0,0,1,30.44,0l48.4,149H480a16,16,0,0,1,9.05,29.2L359,310.35l50.13,148.53A16,16,0,0,1,394,480Z"/>
    400                                 </svg>
    401                                 Icon Settings
    402                                 <span class="fae-section-badge">Drop & Rise Effects</span>
    403                             </h3>
    404                             <div class="fae-icon-settings-row">
    405                                 <div class="fae-setting-group">
    406                                     <label for="fae_cursor_icon">Choose Icon</label>
    407                                     <div class="fae-icon-picker-container">
    408                                         <button type="button" class="fae-icon-picker-trigger" id="fae-icon-picker-trigger">
    409                                             <div class="fae-selected-icon">
    410                                                 <?php
    411                                                 $selected_icon_path = plugin_dir_path(__FILE__) . 'assets/ionicons/' . $selected_icon;
    412                                                 if (file_exists($selected_icon_path)) {
    413                                                     echo file_get_contents($selected_icon_path);
    414                                                 } else {
    415                                                     echo '<svg viewBox="0 0 512 512"><path d="M394,480a16,16,0,0,1-9.39-3L256,383.76,127.39,477a16,16,0,0,1-24.55-18.08L153,310.35,23,221.2A16,16,0,0,1,32,192H192.38l48.4-148.95a16,16,0,0,1,30.44,0l48.4,149H480a16,16,0,0,1,9.05,29.2L359,310.35l50.13,148.53A16,16,0,0,1,394,480Z"/></svg>';
    416                                                 }
    417                                                 ?>
    418                                             </div>
    419                                             <span class="fae-icon-name"><?php echo esc_html(str_replace(array('-outline.svg','-sharp.svg','.svg'), '', $selected_icon)); ?></span>
    420                                             <svg class="fae-dropdown-arrow" viewBox="0 0 512 512">
    421                                                 <path d="M98,190.06a13.06,13.06,0,0,1,9.17-3.95,12.78,12.78,0,0,1,8.95,3.95L256,343.21,395.88,190.06a13.06,13.06,0,0,1,9.17-3.95,12.78,12.78,0,0,1,8.95,3.95,13.61,13.61,0,0,1,0,19.21L264.12,373.18a12.78,12.78,0,0,1-8.95,3.95,13.06,13.06,0,0,1-9.17-3.95L98,209.27A13.61,13.61,0,0,1,98,190.06Z"/>
    422                                             </svg>
    423                                         </button>
    424                                         <input type="hidden" name="fae_cursor_options[icon]" value="<?php echo esc_attr($selected_icon); ?>" id="fae-selected-icon-input">
    425                                        
    426                                         <!-- Icon Picker Modal -->
    427                                         <div class="fae-icon-picker-modal" id="fae-icon-picker-modal">
    428                                             <div class="fae-icon-picker-content">
    429                                                 <div class="fae-icon-picker-header">
    430                                                     <h3>Choose an Icon</h3>
    431                                                     <button type="button" class="fae-icon-picker-close" id="fae-icon-picker-close">
    432                                                         <svg viewBox="0 0 512 512">
    433                                                             <path d="M289.94,256l95-95A24,24,0,0,0,351,127l-95,95-95-95A24,24,0,0,0,127,161l95,95-95,95a24,24,0,1,0,34,34l95-95,95,95a24,24,0,0,0,34-34Z"/>
    434                                                         </svg>
    435                                                     </button>
    436                                                 </div>
    437                                                 <div class="fae-icon-picker-search">
    438                                                     <input type="text" placeholder="Search icons..." id="fae-icon-search">
    439                                                 </div>
    440                                                 <div class="fae-icon-picker-grid" id="fae-icon-picker-grid">
    441                                                   <?php
    442                                                     $icons_dir = plugin_dir_path(__FILE__) . 'assets/ionicons/';
    443                                                     $icons_url = plugin_dir_url(__FILE__) . 'assets/ionicons/';
    444                                                     $icon_files = glob($icons_dir . '*.svg');
    445 
    446                                                     // Define priority icons in the order you want them displayed
    447                                                     $priority_icons = [
    448                                                         'star.svg',
    449                                                         'star-half.svg',
    450                                                         'star-outline.svg',
    451                                                         'sparkles.svg',
    452                                                         'balloon.svg',
    453                                                         'heart-circle.svg',
    454                                                         'heart-half-outline.svg',
    455                                                         'heart-half.svg',
    456                                                         'heart-outline.svg',
    457                                                         'heart.svg'
    458                                                     ];
    459 
    460                                                     // Build ordered icons list.
    461                                                     $ordered_icons = [];
    462 
    463                                                     // If a user-selected icon exists, show it first
    464                                                     if ( ! empty( $selected_icon ) ) {
    465                                                         $selected_path = $icons_dir . $selected_icon;
    466                                                         if ( file_exists( $selected_path ) ) {
    467                                                             $ordered_icons[] = $selected_path;
    468                                                         }
    469                                                     }
    470 
    471                                                     // Add priority icons (skip the selected icon if already added)
    472                                                     foreach ( $priority_icons as $file ) {
    473                                                         $path = $icons_dir . $file;
    474                                                         if ( file_exists( $path ) ) {
    475                                                             if ( empty( $ordered_icons ) || $ordered_icons[0] !== $path ) {
    476                                                                 $ordered_icons[] = $path;
    477                                                             }
    478                                                         }
    479                                                     }
    480 
    481                                                     // Add remaining icons, excluding those already added
    482                                                     foreach ( $icon_files as $path ) {
    483                                                         // Skip if already in ordered list
    484                                                         if ( in_array( $path, $ordered_icons, true ) ) {
    485                                                             continue;
    486                                                         }
    487                                                         $file = basename( $path );
    488                                                         if ( ! in_array( $file, $priority_icons, true ) ) {
    489                                                             $ordered_icons[] = $path;
    490                                                         }
    491                                                     }
    492 
    493                                                     $displayed = 0;
    494                                                     foreach ($ordered_icons as $path) {
    495                                                         $file = basename($path);
    496                                                         $icon_name = str_replace(['-outline.svg', '-sharp.svg', '.svg'], '', $file);
    497                                                         $is_selected = ($file === $selected_icon) ? 'selected' : '';
    498 
    499                                                         // Limit to 5000 icons for performance
    500                                                         if ($displayed >= 5000) break;
    501 
    502                                                         echo '<div class="fae-icon-option ' . $is_selected . '" data-icon="' . esc_attr($file) . '" data-name="' . esc_attr($icon_name) . '">';
    503                                                         echo '<div class="fae-icon-preview">';
    504                                                         echo file_get_contents($path);
    505                                                         echo '</div>';
    506                                                         echo '<span class="fae-icon-label">' . esc_html($icon_name) . '</span>';
    507                                                         echo '</div>';
    508                                                         $displayed++;
    509                                                     }
    510                                                     ?>
    511                                                 </div>
    512                                             </div>
    513                                         </div>
    514                                     </div>
    515                                 </div>
    516                                
    517                                 <div class="fae-setting-group">
    518                                     <label for="fae_cursor_size">Icon Size</label>
    519                                     <select name="fae_cursor_options[size]">
    520                                         <option value="1rem" <?php selected($selected_size, '1rem'); ?>>Small</option>
    521                                         <option value="1.5rem" <?php selected($selected_size, '1.5rem'); ?>>Medium</option>
    522                                         <option value="2rem" <?php selected($selected_size, '2rem'); ?>>Large</option>
    523                                         <option value="2.5rem" <?php selected($selected_size, '2.5rem'); ?>>Extra Large</option>
    524                                     </select>
    525                                 </div>
    526                             </div>
    527                         </div>
    528 
    529                     </div>
    530 
    531                     <!-- Action Buttons -->
    532                     <div class="fae-action-buttons">
    533                         <button type="submit" class="fae-btn fae-btn-primary">
    534                             <svg class="fae-icon" viewBox="0 0 512 512" style="width: 16px; height: 16px;">
    535                                 <path d="M173.898,439.404l-166.4-166.4c-9.997-9.997-9.997-26.206,0-36.204l36.203-36.204c9.997-9.998,26.207-9.998,36.204,0L192,312.69,432.095,72.596c9.997-9.997,26.207-9.997,36.204,0l36.203,36.204c9.997,9.997,9.997,26.206,0,36.204l-294.4,294.401C383.105,449.401,366.896,449.401,356.898,439.404z"/>
    536                             </svg>
    537                             Save Settings
    538                         </button>
    539                        
    540                        
    541                         <button type="button" class="fae-btn fae-btn-secondary fae-reset-settings">
    542                             <svg class="fae-icon" viewBox="0 0 512 512" style="width: 16px; height: 16px;">
    543                                 <path d="M256,48C141.31,48,48,141.31,48,256s93.31,208,208,208,208-93.31,208-208S370.69,48,256,48Zm0,384c-97,0-176-79-176-176S159,80,256,80s176,79,176,176S353,432,256,432Z"/>
    544                             </svg>
    545                             Reset to Defaults
    546                         </button>
    547                     </div>
    548         </form>
    549             </div>
    550         </div>
    551     </div>
    552     <?php
    553 }
    554 
    555 // Font Awesome dependency removed - using embedded Ionicons SVGs
    556 
    557 function fae_cursor_custom_admin_footer($footer_text) {
    558     $screen = get_current_screen();
    559    
    560     if ($screen->id === 'toplevel_page_fae_cursor') {
    561         return '<p>' . __('FaeCursor v1.1', 'faecursor') . '</p><br>' . $footer_text; // Updated to match text domain
    562     }
    563     return $footer_text;
    564 }
    565 add_filter('admin_footer_text', 'fae_cursor_custom_admin_footer');
    566 
    567 function fae_cursor_save_settings_notice() {
    568     // Verify nonce and check if settings were updated
    569     if (
    570         isset($_GET['settings-updated']) &&
    571         '1' === sanitize_text_field(wp_unslash($_GET['settings-updated'])) &&
    572         isset($_GET['_wpnonce']) &&
    573         wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'update-options')
    574     ) {
    575         ?>
    576         <div class="notice notice-success is-dismissible">
    577             <p>
    578                 <?php esc_html_e('Settings saved! ', 'faecursor'); ?>
    579                 <a href="<?php echo esc_url(home_url()); ?>" target="_blank">
    580                     <?php esc_html_e('Visit your site\'s frontend', 'faecursor'); ?>
    581                 </a>
    582                 <?php esc_html_e(' to see the mouse effects in action.', 'faecursor'); ?>
    583             </p>
    584         </div>
    585         <?php
    586     }
    587 }
    588 
    589 
    590 add_action('admin_notices', 'fae_cursor_save_settings_notice');
Note: See TracChangeset for help on using the changeset viewer.