@kilivi/payloadcms-theme-management 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/README.md +346 -845
  2. package/dist/components/AppearanceSessionControls.d.ts +15 -0
  3. package/dist/components/AppearanceSessionControls.d.ts.map +1 -0
  4. package/dist/components/AppearanceSessionControls.js +152 -0
  5. package/dist/components/AppearanceSessionControls.js.map +1 -0
  6. package/dist/components/ColorModeToggle.css +42 -0
  7. package/dist/components/ColorModeToggle.d.ts +40 -0
  8. package/dist/components/ColorModeToggle.d.ts.map +1 -0
  9. package/dist/components/ColorModeToggle.js +111 -0
  10. package/dist/components/ColorModeToggle.js.map +1 -0
  11. package/dist/components/ServerThemeInjector.d.ts +13 -1
  12. package/dist/components/ServerThemeInjector.d.ts.map +1 -1
  13. package/dist/components/ServerThemeInjector.js +65 -15
  14. package/dist/components/ServerThemeInjector.js.map +1 -1
  15. package/dist/components/ThemeLivePreviewSync.d.ts +4 -0
  16. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  17. package/dist/components/ThemeLivePreviewSync.js +28 -4
  18. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  19. package/dist/components/ThemeSwitcher.d.ts +36 -0
  20. package/dist/components/ThemeSwitcher.d.ts.map +1 -0
  21. package/dist/components/ThemeSwitcher.js +89 -0
  22. package/dist/components/ThemeSwitcher.js.map +1 -0
  23. package/dist/components/typographyPreviewUtils.d.ts +5 -0
  24. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  25. package/dist/components/typographyPreviewUtils.js +24 -11
  26. package/dist/components/typographyPreviewUtils.js.map +1 -1
  27. package/dist/constants/devicePreviews.d.ts +35 -0
  28. package/dist/constants/devicePreviews.d.ts.map +1 -0
  29. package/dist/constants/devicePreviews.js +43 -0
  30. package/dist/constants/devicePreviews.js.map +1 -0
  31. package/dist/constants/themeFonts.d.ts +94 -168
  32. package/dist/constants/themeFonts.d.ts.map +1 -1
  33. package/dist/constants/themeFonts.js +260 -140
  34. package/dist/constants/themeFonts.js.map +1 -1
  35. package/dist/endpoints/themeFonts.d.ts +5 -0
  36. package/dist/endpoints/themeFonts.d.ts.map +1 -0
  37. package/dist/endpoints/themeFonts.js +138 -0
  38. package/dist/endpoints/themeFonts.js.map +1 -0
  39. package/dist/extended-presets.d.ts +0 -4
  40. package/dist/extended-presets.d.ts.map +1 -1
  41. package/dist/extended-presets.js +1 -18
  42. package/dist/extended-presets.js.map +1 -1
  43. package/dist/extended-themes.d.ts.map +1 -1
  44. package/dist/extended-themes.js +428 -0
  45. package/dist/extended-themes.js.map +1 -1
  46. package/dist/fields/AccessibilityAuditField.d.ts +2 -0
  47. package/dist/fields/AccessibilityAuditField.d.ts.map +1 -0
  48. package/dist/fields/AccessibilityAuditField.js +303 -0
  49. package/dist/fields/AccessibilityAuditField.js.map +1 -0
  50. package/dist/fields/AppearancePreviewField.d.ts +2 -0
  51. package/dist/fields/AppearancePreviewField.d.ts.map +1 -0
  52. package/dist/fields/AppearancePreviewField.js +826 -0
  53. package/dist/fields/AppearancePreviewField.js.map +1 -0
  54. package/dist/fields/ColorPickerField.d.ts.map +1 -1
  55. package/dist/fields/ColorPickerField.js +89 -85
  56. package/dist/fields/ColorPickerField.js.map +1 -1
  57. package/dist/fields/FontSelectField.css +155 -0
  58. package/dist/fields/FontSelectField.d.ts +8 -0
  59. package/dist/fields/FontSelectField.d.ts.map +1 -1
  60. package/dist/fields/FontSelectField.js +223 -188
  61. package/dist/fields/FontSelectField.js.map +1 -1
  62. package/dist/fields/PaletteGeneratorField.d.ts +7 -0
  63. package/dist/fields/PaletteGeneratorField.d.ts.map +1 -0
  64. package/dist/fields/PaletteGeneratorField.js +327 -0
  65. package/dist/fields/PaletteGeneratorField.js.map +1 -0
  66. package/dist/fields/StylePresetField.d.ts +3 -0
  67. package/dist/fields/StylePresetField.d.ts.map +1 -0
  68. package/dist/fields/StylePresetField.js +463 -0
  69. package/dist/fields/StylePresetField.js.map +1 -0
  70. package/dist/fields/ThemeColorPickerField.css +115 -35
  71. package/dist/fields/ThemeColorPickerField.d.ts.map +1 -1
  72. package/dist/fields/ThemeColorPickerField.js +204 -70
  73. package/dist/fields/ThemeColorPickerField.js.map +1 -1
  74. package/dist/fields/ThemeExportField.d.ts +6 -0
  75. package/dist/fields/ThemeExportField.d.ts.map +1 -0
  76. package/dist/fields/ThemeExportField.js +202 -0
  77. package/dist/fields/ThemeExportField.js.map +1 -0
  78. package/dist/fields/ThemePresetImportField.d.ts.map +1 -1
  79. package/dist/fields/ThemePresetImportField.js +56 -25
  80. package/dist/fields/ThemePresetImportField.js.map +1 -1
  81. package/dist/fields/ThemePreviewField.css +444 -5
  82. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  83. package/dist/fields/ThemePreviewField.js +265 -170
  84. package/dist/fields/ThemePreviewField.js.map +1 -1
  85. package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
  86. package/dist/fields/ThemeTokenSelectField.js +278 -80
  87. package/dist/fields/ThemeTokenSelectField.js.map +1 -1
  88. package/dist/fields/colorFieldHelpers.d.ts +90 -0
  89. package/dist/fields/colorFieldHelpers.d.ts.map +1 -0
  90. package/dist/fields/colorFieldHelpers.js +133 -0
  91. package/dist/fields/colorFieldHelpers.js.map +1 -0
  92. package/dist/fields/colorModeFields.d.ts +1 -1
  93. package/dist/fields/colorModeFields.d.ts.map +1 -1
  94. package/dist/fields/colorModeFields.js +49 -7
  95. package/dist/fields/colorModeFields.js.map +1 -1
  96. package/dist/fields/themeConfigurationField.d.ts +8 -1
  97. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  98. package/dist/fields/themeConfigurationField.js +1209 -228
  99. package/dist/fields/themeConfigurationField.js.map +1 -1
  100. package/dist/globals/ThemeSettings/FontPicker/FontPreview.d.ts.map +1 -1
  101. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js +8 -4
  102. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js.map +1 -1
  103. package/dist/globals/ThemeSettings/FontPicker/index.d.ts.map +1 -1
  104. package/dist/globals/ThemeSettings/FontPicker/index.js +81 -26
  105. package/dist/globals/ThemeSettings/FontPicker/index.js.map +1 -1
  106. package/dist/globals/ThemeSettings/FontPicker/styles.css +98 -41
  107. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts +6 -0
  108. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts.map +1 -1
  109. package/dist/globals/ThemeSettings/FontPicker/translations.js +14 -2
  110. package/dist/globals/ThemeSettings/FontPicker/translations.js.map +1 -1
  111. package/dist/globals/ThemeSettings/config.d.ts.map +1 -1
  112. package/dist/globals/ThemeSettings/config.js +76 -79
  113. package/dist/globals/ThemeSettings/config.js.map +1 -1
  114. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.d.ts.map +1 -1
  115. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js +63 -62
  116. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js.map +1 -1
  117. package/dist/globals/ThemeSettings/fields/TypographyInfoField.d.ts.map +1 -1
  118. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js +22 -25
  119. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js.map +1 -1
  120. package/dist/hooks/useAppearanceSession.d.ts +50 -0
  121. package/dist/hooks/useAppearanceSession.d.ts.map +1 -0
  122. package/dist/hooks/useAppearanceSession.js +91 -0
  123. package/dist/hooks/useAppearanceSession.js.map +1 -0
  124. package/dist/hooks/useNativeLivePreview.d.ts +15 -0
  125. package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
  126. package/dist/hooks/useNativeLivePreview.js +18 -0
  127. package/dist/hooks/useNativeLivePreview.js.map +1 -0
  128. package/dist/hooks/useThemeFonts.d.ts +10 -0
  129. package/dist/hooks/useThemeFonts.d.ts.map +1 -0
  130. package/dist/hooks/useThemeFonts.js +36 -0
  131. package/dist/hooks/useThemeFonts.js.map +1 -0
  132. package/dist/hooks/useThemeTranslations.d.ts +13 -0
  133. package/dist/hooks/useThemeTranslations.d.ts.map +1 -0
  134. package/dist/hooks/useThemeTranslations.js +22 -0
  135. package/dist/hooks/useThemeTranslations.js.map +1 -0
  136. package/dist/i18n.d.ts +53 -0
  137. package/dist/i18n.d.ts.map +1 -0
  138. package/dist/i18n.js +53 -0
  139. package/dist/i18n.js.map +1 -0
  140. package/dist/index.d.ts +13 -4
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +59 -29
  143. package/dist/index.js.map +1 -1
  144. package/dist/next-fonts.d.ts +3 -0
  145. package/dist/next-fonts.d.ts.map +1 -0
  146. package/dist/next-fonts.js +360 -0
  147. package/dist/next-fonts.js.map +1 -0
  148. package/dist/payload-types.d.ts +34 -6
  149. package/dist/payload-types.d.ts.map +1 -1
  150. package/dist/payload-types.js.map +1 -1
  151. package/dist/presets.d.ts +14 -0
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js.map +1 -1
  154. package/dist/providers/Theme/index.d.ts.map +1 -1
  155. package/dist/providers/Theme/index.js +75 -11
  156. package/dist/providers/Theme/index.js.map +1 -1
  157. package/dist/providers/Theme/themeConfig.d.ts.map +1 -1
  158. package/dist/providers/Theme/themeConfig.js +104 -0
  159. package/dist/providers/Theme/themeConfig.js.map +1 -1
  160. package/dist/providers/Theme/types.d.ts +1 -1
  161. package/dist/providers/Theme/types.d.ts.map +1 -1
  162. package/dist/providers/Theme/types.js +10 -1
  163. package/dist/providers/Theme/types.js.map +1 -1
  164. package/dist/style-presets.d.ts +75 -0
  165. package/dist/style-presets.d.ts.map +1 -0
  166. package/dist/style-presets.js +738 -0
  167. package/dist/style-presets.js.map +1 -0
  168. package/dist/translations.d.ts +164 -1
  169. package/dist/translations.d.ts.map +1 -1
  170. package/dist/translations.js +320 -9
  171. package/dist/translations.js.map +1 -1
  172. package/dist/tweakcn-special-themes.d.ts.map +1 -1
  173. package/dist/tweakcn-special-themes.js +585 -9
  174. package/dist/tweakcn-special-themes.js.map +1 -1
  175. package/dist/types/css.d.js +3 -0
  176. package/dist/types/css.d.js.map +1 -0
  177. package/dist/types.d.ts +26 -0
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/dist/utilities/getThemeSettings.d.ts.map +1 -1
  182. package/dist/utilities/getThemeSettings.js +2 -14
  183. package/dist/utilities/getThemeSettings.js.map +1 -1
  184. package/dist/utils/contrast.d.ts +63 -0
  185. package/dist/utils/contrast.d.ts.map +1 -0
  186. package/dist/utils/contrast.js +158 -0
  187. package/dist/utils/contrast.js.map +1 -0
  188. package/dist/utils/exportTokens.d.ts +30 -0
  189. package/dist/utils/exportTokens.d.ts.map +1 -0
  190. package/dist/utils/exportTokens.js +183 -0
  191. package/dist/utils/exportTokens.js.map +1 -0
  192. package/dist/utils/generatePalette.d.ts +46 -0
  193. package/dist/utils/generatePalette.d.ts.map +1 -0
  194. package/dist/utils/generatePalette.js +127 -0
  195. package/dist/utils/generatePalette.js.map +1 -0
  196. package/dist/utils/generateThemeCSS.d.ts.map +1 -1
  197. package/dist/utils/generateThemeCSS.js +2 -40
  198. package/dist/utils/generateThemeCSS.js.map +1 -1
  199. package/dist/utils/livePreviewThemeRuntime.d.ts +22 -3
  200. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  201. package/dist/utils/livePreviewThemeRuntime.js +48 -15
  202. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  203. package/dist/utils/localizedText.d.ts +8 -0
  204. package/dist/utils/localizedText.d.ts.map +1 -0
  205. package/dist/utils/localizedText.js +21 -0
  206. package/dist/utils/localizedText.js.map +1 -0
  207. package/dist/utils/resolveThemeConfiguration.d.ts +60 -1
  208. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  209. package/dist/utils/resolveThemeConfiguration.js +53 -13
  210. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  211. package/dist/utils/themeColors.d.ts +3 -0
  212. package/dist/utils/themeColors.d.ts.map +1 -1
  213. package/dist/utils/themeColors.js +30 -1
  214. package/dist/utils/themeColors.js.map +1 -1
  215. package/dist/utils/themeHtmlAttributes.d.ts +32 -12
  216. package/dist/utils/themeHtmlAttributes.d.ts.map +1 -1
  217. package/dist/utils/themeHtmlAttributes.js +38 -5
  218. package/dist/utils/themeHtmlAttributes.js.map +1 -1
  219. package/dist/utils/themeUtils.d.ts +3 -5
  220. package/dist/utils/themeUtils.d.ts.map +1 -1
  221. package/dist/utils/themeUtils.js +474 -6
  222. package/dist/utils/themeUtils.js.map +1 -1
  223. package/dist/utils/tweakcnConverter.d.ts +1 -3
  224. package/dist/utils/tweakcnConverter.d.ts.map +1 -1
  225. package/dist/utils/tweakcnConverter.js +1 -3
  226. package/dist/utils/tweakcnConverter.js.map +1 -1
  227. package/package.json +15 -5
  228. package/dist/components/ThemePreview.d.ts +0 -3
  229. package/dist/components/ThemePreview.d.ts.map +0 -1
  230. package/dist/components/ThemePreview.js +0 -321
  231. package/dist/components/ThemePreview.js.map +0 -1
  232. package/dist/plugin.d.ts +0 -2
  233. package/dist/plugin.d.ts.map +0 -1
  234. package/dist/plugin.js +0 -3
  235. package/dist/plugin.js.map +0 -1
@@ -5,7 +5,7 @@ import { borderRadiusPresets } from '../providers/Theme/themeConfig.js';
5
5
  const cssRules = [];
6
6
  const customization = config.customization;
7
7
  const themeSelector = config.theme ? `:root[data-theme='${config.theme}']` : ':root';
8
- // Start root CSS rules
8
+ // ── :root block — CSS custom properties ─────────────────────────────────
9
9
  cssRules.push(`${themeSelector} {`);
10
10
  applyCustomizationRootRules(cssRules, customization);
11
11
  applyBorderRadiusRule(cssRules, config);
@@ -13,12 +13,17 @@ import { borderRadiusPresets } from '../providers/Theme/themeConfig.js';
13
13
  applySpacingRule(cssRules, config);
14
14
  applyAnimationRule(cssRules, config);
15
15
  applyTypographyRules(cssRules, config);
16
+ applyVisualEffectsCSSVars(cssRules, config);
17
+ applyComponentStyleCSSVars(cssRules, config);
16
18
  cssRules.push('}');
17
- // Color mode specific overrides
19
+ // ── Element-level effect rules (outside :root) ──────────────────────────
20
+ applyTypographyBaseRules(cssRules, config);
21
+ applyEffectElementRules(cssRules, config);
22
+ // ── Color mode overrides ────────────────────────────────────────────────
18
23
  applyDarkModeOverrides(cssRules, config, customization, themeSelector);
19
- // Animation level: none - respect prefers-reduced-motion
24
+ // ── Animation: none — respect prefers-reduced-motion ───────────────────
20
25
  applyReducedMotionOverride(cssRules, config);
21
- // Custom CSS
26
+ // ── Custom CSS ──────────────────────────────────────────────────────────
22
27
  appendCustomCSS(cssRules, customization);
23
28
  return cssRules.join('\n');
24
29
  }
@@ -109,6 +114,10 @@ function applySpacingRule(rules, config) {
109
114
  const multiplier = spacingMultipliers[config.spacing] ?? '1';
110
115
  rules.push(` --spacing-scale: ${multiplier};`);
111
116
  rules.push(` --theme-spacing: ${multiplier};`);
117
+ // Ready-to-use rhythm tokens so the spacing choice is visible without extra math.
118
+ rules.push(` --section-spacing: calc(clamp(3rem, 8vw, 6rem) * ${multiplier});`);
119
+ rules.push(` --card-padding: calc(1.5rem * ${multiplier});`);
120
+ rules.push(` --stack-gap: calc(1rem * ${multiplier});`);
112
121
  }
113
122
  function applyAnimationRule(rules, config) {
114
123
  if (!config.animationLevel) return;
@@ -128,7 +137,7 @@ function applyAnimationRule(rules, config) {
128
137
  easing: 'ease-out',
129
138
  scale: '1'
130
139
  },
131
- full: {
140
+ high: {
132
141
  duration: '0.3s',
133
142
  easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
134
143
  scale: '1.2'
@@ -136,7 +145,6 @@ function applyAnimationRule(rules, config) {
136
145
  };
137
146
  const level = config.animationLevel;
138
147
  const settings = animationSettings[level] ?? animationSettings.medium;
139
- if (!settings) return;
140
148
  rules.push(` --animation-duration: ${settings.duration};`);
141
149
  rules.push(` --animation-easing: ${settings.easing};`);
142
150
  rules.push(` --animation-scale: ${settings.scale};`);
@@ -171,6 +179,466 @@ function applyTypographyRules(rules, config) {
171
179
  }
172
180
  });
173
181
  }
182
+ if (typography.headingWeight) {
183
+ rules.push(` --typography-headingWeight: ${typography.headingWeight};`);
184
+ }
185
+ if (typography.headingLetterSpacing) {
186
+ rules.push(` --typography-headingLetterSpacing: ${typography.headingLetterSpacing};`);
187
+ }
188
+ if (typography.headingTransform) {
189
+ rules.push(` --typography-headingTransform: ${typography.headingTransform};`);
190
+ }
191
+ }
192
+ /**
193
+ * Zero-specificity base rules (`:where()`), so the chosen fonts show up on a site
194
+ * that has no typography CSS of its own, while any rule the site does have wins.
195
+ */ function applyTypographyBaseRules(rules, config) {
196
+ // Only when the editor chose typography explicitly: sites that never touched it
197
+ // keep rendering exactly as before.
198
+ if (!config.typography || !config.typographyOverridden) return;
199
+ rules.push('');
200
+ rules.push('/* ── Typography base (zero specificity — site CSS always wins) ── */');
201
+ rules.push(':where(html[data-theme]) :where(body) {');
202
+ rules.push(' font-family: var(--typography-fontFamily, system-ui, sans-serif);');
203
+ rules.push(' font-size: calc(var(--typography-baseFontSize, 16px) * var(--theme-font-scale, 1));');
204
+ rules.push(' line-height: var(--typography-lineHeight, 1.6);');
205
+ rules.push('}');
206
+ rules.push(':where(html[data-theme]) :where(h1, h2, h3, h4, h5, h6) {');
207
+ rules.push(' font-family: var(--typography-headingFamily, var(--typography-fontFamily, inherit));');
208
+ rules.push(' font-weight: var(--typography-headingWeight, var(--typography-fontWeights-bold, 700));');
209
+ rules.push(' letter-spacing: var(--typography-headingLetterSpacing, normal);');
210
+ rules.push(' text-transform: var(--typography-headingTransform, none);');
211
+ rules.push('}');
212
+ }
213
+ function getVE(config) {
214
+ return config.visualEffects ?? null;
215
+ }
216
+ function getCS(config) {
217
+ return config.componentStyles ?? null;
218
+ }
219
+ const SHADOW_MAP = {
220
+ none: 'none',
221
+ subtle: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
222
+ medium: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
223
+ strong: '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)',
224
+ dramatic: '0 25px 50px -12px rgba(0,0,0,0.25)'
225
+ };
226
+ const BLUR_PX_MAP = {
227
+ none: '0px',
228
+ slight: '4px',
229
+ medium: '8px',
230
+ strong: '16px',
231
+ heavy: '24px'
232
+ };
233
+ /**
234
+ * CSS custom properties for visual effects (emitted inside :root {})
235
+ */ function applyVisualEffectsCSSVars(rules, config) {
236
+ const ve = getVE(config);
237
+ if (!ve) return;
238
+ if (ve.effectStyle) rules.push(` --effect-style: ${ve.effectStyle};`);
239
+ if (ve.shadowIntensity) {
240
+ rules.push(` --shadow-intensity: ${ve.shadowIntensity};`);
241
+ const shadow = SHADOW_MAP[ve.shadowIntensity];
242
+ if (shadow) rules.push(` --card-shadow: ${shadow};`);
243
+ }
244
+ if (ve.backdropBlur) {
245
+ const blurPx = BLUR_PX_MAP[ve.backdropBlur] ?? '0px';
246
+ rules.push(` --backdrop-blur: ${blurPx};`);
247
+ // Token form (none|slight|...) — read by the client ThemeProvider to mirror
248
+ // the SSR data-backdrop-blur attribute.
249
+ rules.push(` --backdrop-blur-level: ${ve.backdropBlur};`);
250
+ }
251
+ if (ve.borderStyle) rules.push(` --border-style: ${ve.borderStyle};`);
252
+ if (ve.borderWidth) rules.push(` --border-width: ${ve.borderWidth};`);
253
+ if (ve.glassOpacity != null) {
254
+ const opacity = Math.min(100, Math.max(0, ve.glassOpacity)) / 100;
255
+ rules.push(` --glass-opacity: ${opacity.toFixed(2)};`);
256
+ }
257
+ // Always emit --card-bg-opacity and --card-backdrop-filter so .glass-card / .glass
258
+ // fallback rules in consumer globals.css can be overridden regardless of cached state.
259
+ // Glass effect: translucent card; all other effects: reset to opaque/none.
260
+ const opacity = ve.glassOpacity != null ? (ve.glassOpacity / 100).toFixed(2) : '0.60';
261
+ const blur = BLUR_PX_MAP[ve.backdropBlur ?? 'medium'] ?? '8px';
262
+ rules.push(` --card-bg-opacity: ${opacity};`);
263
+ rules.push(` --card-backdrop-filter: blur(${blur});`);
264
+ rules.push(` --card-bg-opacity-non-glass: 1.0;`);
265
+ rules.push(` --card-backdrop-filter-non-glass: none;`);
266
+ }
267
+ /**
268
+ * CSS custom properties for component styles (emitted inside :root {})
269
+ */ function applyComponentStyleCSSVars(rules, config) {
270
+ // Brand gradient composite — always emitted so gradient buttons / cards / text
271
+ // can reference a single token. Falls back to primary→accent when the editor
272
+ // has not set explicit gradient stops.
273
+ rules.push(' --gradient-brand: linear-gradient(' + 'var(--gradient-angle, 135deg), ' + 'var(--gradient-from, var(--primary)), ' + 'var(--gradient-via, var(--gradient-from, var(--primary))), ' + 'var(--gradient-to, var(--accent)));');
274
+ const cs = getCS(config);
275
+ if (!cs) return;
276
+ if (cs.buttonVariant) rules.push(` --button-variant: ${cs.buttonVariant};`);
277
+ if (cs.buttonSize) rules.push(` --button-size: ${cs.buttonSize};`);
278
+ if (cs.cardStyle) rules.push(` --card-style: ${cs.cardStyle};`);
279
+ if (cs.cardHoverEffect) rules.push(` --card-hover-effect: ${cs.cardHoverEffect};`);
280
+ if (cs.imageStyle) rules.push(` --image-style: ${cs.imageStyle};`);
281
+ if (cs.navbarStyle) rules.push(` --navbar-style: ${cs.navbarStyle};`);
282
+ if (cs.footerStyle) rules.push(` --footer-style: ${cs.footerStyle};`);
283
+ if (cs.linkStyle) rules.push(` --link-style: ${cs.linkStyle};`);
284
+ if (cs.enableScrollReveal != null) {
285
+ rules.push(` --scroll-reveal: ${cs.enableScrollReveal ? '1' : '0'};`);
286
+ }
287
+ if (cs.enableHoverAnimations != null) {
288
+ rules.push(` --hover-animations: ${cs.enableHoverAnimations ? '1' : '0'};`);
289
+ }
290
+ }
291
+ /**
292
+ * Element-level CSS overrides based on effect + component styles.
293
+ * These rules are emitted OUTSIDE the :root {} block so they target
294
+ * actual DOM elements. They rely on data-effect-style / data-card-style
295
+ * attributes set on <html> by layout.tsx (SSR) and ThemeProvider (client).
296
+ */ function applyEffectElementRules(rules, config) {
297
+ const ve = getVE(config);
298
+ const cs = getCS(config);
299
+ if (!ve && !cs) return;
300
+ rules.push('');
301
+ rules.push('/* ── Effect element overrides ── */');
302
+ // ── Card border style / width (effect rules below may still replace it) ──
303
+ // Skipped for the field defaults (solid / 1px) so existing cards are unchanged.
304
+ if (ve?.borderStyle && (ve.borderStyle !== 'solid' || (ve.borderWidth ?? '1px') !== '1px')) {
305
+ rules.push(`html[data-border-style] [data-card] {`);
306
+ rules.push(` border-style: var(--border-style, solid);`);
307
+ rules.push(` border-width: var(--border-width, 1px);`);
308
+ rules.push(` border-color: var(--border);`);
309
+ rules.push(`}`);
310
+ }
311
+ // ── Glass effect ──────────────────────────────────────────────────────
312
+ // NOTE: All effect-style blocks are ALWAYS emitted regardless of the saved
313
+ // effectStyle value. The CSS selectors are scoped by html[data-effect-style='X']
314
+ // so only the matching data-attribute activates the rules. This means the CSS
315
+ // is always correct even when stale cached/draft data is served.
316
+ {
317
+ const opacity = ve?.glassOpacity != null ? ve.glassOpacity : 60;
318
+ const blur = BLUR_PX_MAP[ve?.backdropBlur ?? 'strong'] ?? '16px';
319
+ rules.push(`html[data-effect-style='glass'] [data-card],`);
320
+ rules.push(`html[data-effect-style='glass'] .glass-card,`);
321
+ rules.push(`html[data-effect-style='glass'] .glass {`);
322
+ rules.push(` background-color: color-mix(in srgb, var(--card) ${opacity}%, transparent) !important;`);
323
+ rules.push(` backdrop-filter: blur(${blur}) saturate(150%) !important;`);
324
+ rules.push(` -webkit-backdrop-filter: blur(${blur}) saturate(150%) !important;`);
325
+ rules.push(` border-color: color-mix(in srgb, var(--border) 40%, transparent) !important;`);
326
+ rules.push(` box-shadow: 0 4px 24px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.1) !important;`);
327
+ rules.push(`}`);
328
+ rules.push(`html[data-effect-style='glass'] [data-card]:hover,`);
329
+ rules.push(`html[data-effect-style='glass'] .glass-card:hover,`);
330
+ rules.push(`html[data-effect-style='glass'] .glass:hover {`);
331
+ rules.push(` background-color: color-mix(in srgb, var(--card) ${Math.min(opacity + 10, 90)}%, transparent) !important;`);
332
+ rules.push(` box-shadow: 0 8px 32px rgba(0,0,0,0.12), inset 0 1px 0 rgba(255,255,255,0.15) !important;`);
333
+ rules.push(`}`);
334
+ // Apply the glass surface to the navbar container, but never to a <nav>
335
+ // nested inside a <header> (or <footer>) — otherwise the inner nav paints a
336
+ // second glass layer on top of the header's, producing a visible double-tint.
337
+ // A standalone top-level <nav> (header-less layouts) still gets the surface.
338
+ rules.push(`html[data-effect-style='glass'] header, html[data-effect-style='glass'] nav:not(header nav):not(footer nav) {`);
339
+ rules.push(` background-color: color-mix(in srgb, var(--background) 65%, transparent) !important;`);
340
+ rules.push(` backdrop-filter: blur(${blur}) saturate(150%) !important;`);
341
+ rules.push(` -webkit-backdrop-filter: blur(${blur}) saturate(150%) !important;`);
342
+ rules.push(`}`);
343
+ }
344
+ // ── Neumorphic effect ──────────────────────────────────────────────────
345
+ {
346
+ rules.push(`html[data-effect-style='neumorphic'] [data-card],`);
347
+ rules.push(`html[data-effect-style='neumorphic'] .glass-card,`);
348
+ rules.push(`html[data-effect-style='neumorphic'] .glass {`);
349
+ rules.push(` background-color: var(--background) !important;`);
350
+ rules.push(` border: none !important;`);
351
+ rules.push(` backdrop-filter: none !important;`);
352
+ rules.push(` -webkit-backdrop-filter: none !important;`);
353
+ rules.push(` box-shadow: 6px 6px 12px rgba(0,0,0,0.12), -6px -6px 12px rgba(255,255,255,0.75) !important;`);
354
+ rules.push(`}`);
355
+ rules.push(`html[data-effect-style='neumorphic'] [data-card]:hover,`);
356
+ rules.push(`html[data-effect-style='neumorphic'] .glass-card:hover,`);
357
+ rules.push(`html[data-effect-style='neumorphic'] .glass:hover {`);
358
+ rules.push(` box-shadow: 8px 8px 16px rgba(0,0,0,0.15), -8px -8px 16px rgba(255,255,255,0.85) !important;`);
359
+ rules.push(`}`);
360
+ rules.push(`html[data-effect-style='neumorphic'][data-theme-mode='dark'] [data-card],`);
361
+ rules.push(`html[data-effect-style='neumorphic'].dark [data-card],`);
362
+ rules.push(`html[data-effect-style='neumorphic'][data-theme-mode='dark'] .glass-card,`);
363
+ rules.push(`html[data-effect-style='neumorphic'].dark .glass-card {`);
364
+ rules.push(` box-shadow: 6px 6px 12px rgba(0,0,0,0.4), -6px -6px 12px rgba(255,255,255,0.04) !important;`);
365
+ rules.push(`}`);
366
+ }
367
+ // ── Clay effect ────────────────────────────────────────────────────────
368
+ {
369
+ rules.push(`html[data-effect-style='clay'] [data-card],`);
370
+ rules.push(`html[data-effect-style='clay'] .glass-card,`);
371
+ rules.push(`html[data-effect-style='clay'] .glass {`);
372
+ rules.push(` border-radius: var(--radius-large, 1.5rem) !important;`);
373
+ rules.push(` box-shadow: 4px 4px 0px rgba(0,0,0,0.22), 0 8px 20px rgba(0,0,0,0.08) !important;`);
374
+ rules.push(` border: none !important;`);
375
+ rules.push(` backdrop-filter: none !important;`);
376
+ rules.push(` -webkit-backdrop-filter: none !important;`);
377
+ rules.push(` transition: transform 200ms ease, box-shadow 200ms ease !important;`);
378
+ rules.push(`}`);
379
+ rules.push(`html[data-effect-style='clay'] [data-card]:hover,`);
380
+ rules.push(`html[data-effect-style='clay'] .glass-card:hover,`);
381
+ rules.push(`html[data-effect-style='clay'] .glass:hover {`);
382
+ rules.push(` transform: translateY(-3px) !important;`);
383
+ rules.push(` box-shadow: 6px 6px 0px rgba(0,0,0,0.25), 0 12px 24px rgba(0,0,0,0.12) !important;`);
384
+ rules.push(`}`);
385
+ }
386
+ // ── Elevated effect ────────────────────────────────────────────────────
387
+ {
388
+ const shadow = ve?.shadowIntensity ? SHADOW_MAP[ve.shadowIntensity] ?? SHADOW_MAP.medium : SHADOW_MAP.medium;
389
+ rules.push(`html[data-effect-style='elevated'] [data-card],`);
390
+ rules.push(`html[data-effect-style='elevated'] .glass-card,`);
391
+ rules.push(`html[data-effect-style='elevated'] .glass {`);
392
+ rules.push(` box-shadow: ${shadow} !important;`);
393
+ rules.push(` border: none !important;`);
394
+ rules.push(` backdrop-filter: none !important;`);
395
+ rules.push(` -webkit-backdrop-filter: none !important;`);
396
+ rules.push(` transition: box-shadow 200ms ease, transform 200ms ease !important;`);
397
+ rules.push(`}`);
398
+ rules.push(`html[data-effect-style='elevated'] [data-card]:hover,`);
399
+ rules.push(`html[data-effect-style='elevated'] .glass-card:hover,`);
400
+ rules.push(`html[data-effect-style='elevated'] .glass:hover {`);
401
+ rules.push(` box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1) !important;`);
402
+ rules.push(` transform: translateY(-2px) !important;`);
403
+ rules.push(`}`);
404
+ }
405
+ // ── Flat effect ────────────────────────────────────────────────────────
406
+ {
407
+ rules.push(`html[data-effect-style='flat'] [data-card],`);
408
+ rules.push(`html[data-effect-style='flat'] .glass-card,`);
409
+ rules.push(`html[data-effect-style='flat'] .glass {`);
410
+ rules.push(` box-shadow: none !important;`);
411
+ rules.push(` backdrop-filter: none !important;`);
412
+ rules.push(` -webkit-backdrop-filter: none !important;`);
413
+ rules.push(`}`);
414
+ }
415
+ // ── Border overrides ───────────────────────────────────────────────────
416
+ if (ve?.borderWidth && ve.borderWidth !== '1px' && ve.borderWidth !== '0px') {
417
+ rules.push(`html[data-border-width='${ve.borderWidth}'] [data-card],`);
418
+ rules.push(`html[data-border-width='${ve.borderWidth}'] .glass-card {`);
419
+ rules.push(` border-width: ${ve.borderWidth} !important;`);
420
+ rules.push(`}`);
421
+ }
422
+ // ── Card style: bordered (used by brutalism etc.) ──────────────────────
423
+ if (cs?.cardStyle === 'bordered') {
424
+ const bw = ve?.borderWidth ?? '2px';
425
+ rules.push(`html[data-card-style='bordered'] [data-card],`);
426
+ rules.push(`html[data-card-style='bordered'] .glass-card {`);
427
+ rules.push(` border-width: ${bw} !important;`);
428
+ rules.push(` border-style: solid !important;`);
429
+ rules.push(` border-color: var(--foreground) !important;`);
430
+ rules.push(` box-shadow: 4px 4px 0 var(--foreground) !important;`);
431
+ rules.push(` border-radius: 0 !important;`);
432
+ rules.push(` background-color: var(--card) !important;`);
433
+ rules.push(` backdrop-filter: none !important;`);
434
+ rules.push(` -webkit-backdrop-filter: none !important;`);
435
+ rules.push(`}`);
436
+ rules.push(`html[data-card-style='bordered'] [data-card]:hover,`);
437
+ rules.push(`html[data-card-style='bordered'] .glass-card:hover {`);
438
+ rules.push(` box-shadow: 6px 6px 0 var(--foreground) !important;`);
439
+ rules.push(` transform: translate(-2px, -2px) !important;`);
440
+ rules.push(`}`);
441
+ }
442
+ // ── Card style: gradient-border ────────────────────────────────────────
443
+ if (cs?.cardStyle === 'gradient-border') {
444
+ rules.push(`html[data-card-style='gradient-border'] [data-card],`);
445
+ rules.push(`html[data-card-style='gradient-border'] .glass-card {`);
446
+ rules.push(` border-image: var(--gradient-brand) 1 !important;`);
447
+ rules.push(` border-width: 2px !important;`);
448
+ rules.push(` border-style: solid !important;`);
449
+ rules.push(`}`);
450
+ }
451
+ // ── Navbar styles ──────────────────────────────────────────────────────
452
+ const navbarStyle = cs?.navbarStyle;
453
+ if (navbarStyle === 'blur') {
454
+ rules.push(`html[data-navbar-style='blur'] header {`);
455
+ rules.push(` background-color: color-mix(in srgb, var(--background) 70%, transparent) !important;`);
456
+ rules.push(` backdrop-filter: blur(12px) !important;`);
457
+ rules.push(` -webkit-backdrop-filter: blur(12px) !important;`);
458
+ rules.push(`}`);
459
+ } else if (navbarStyle === 'transparent') {
460
+ rules.push(`html[data-navbar-style='transparent'] header {`);
461
+ rules.push(` background-color: transparent !important;`);
462
+ rules.push(` border-bottom-color: transparent !important;`);
463
+ rules.push(`}`);
464
+ } else if (navbarStyle === 'floating') {
465
+ rules.push(`html[data-navbar-style='floating'] header {`);
466
+ rules.push(` margin: 12px 16px 0;`);
467
+ rules.push(` border-radius: var(--radius-large, 1.5rem);`);
468
+ rules.push(` border: 1px solid var(--border);`);
469
+ rules.push(` box-shadow: 0 4px 24px rgba(0,0,0,0.08);`);
470
+ rules.push(`}`);
471
+ } else if (navbarStyle === 'minimal') {
472
+ rules.push(`html[data-navbar-style='minimal'] header {`);
473
+ rules.push(` border-bottom-color: transparent !important;`);
474
+ rules.push(` box-shadow: none !important;`);
475
+ rules.push(`}`);
476
+ }
477
+ // ── Button variant: brutal ─────────────────────────────────────────────
478
+ if (cs?.buttonVariant === 'brutal') {
479
+ rules.push(`html[data-button-variant='brutal'] [data-btn] {`);
480
+ rules.push(` border: 2px solid var(--foreground) !important;`);
481
+ rules.push(` border-radius: 0 !important;`);
482
+ rules.push(` box-shadow: 3px 3px 0 var(--foreground) !important;`);
483
+ rules.push(` transition: box-shadow 100ms ease, transform 100ms ease !important;`);
484
+ rules.push(`}`);
485
+ rules.push(`html[data-button-variant='brutal'] [data-btn]:hover {`);
486
+ rules.push(` box-shadow: 5px 5px 0 var(--foreground) !important;`);
487
+ rules.push(` transform: translate(-2px, -2px) !important;`);
488
+ rules.push(`}`);
489
+ rules.push(`html[data-button-variant='brutal'] [data-btn]:active {`);
490
+ rules.push(` box-shadow: 1px 1px 0 var(--foreground) !important;`);
491
+ rules.push(` transform: translate(2px, 2px) !important;`);
492
+ rules.push(`}`);
493
+ }
494
+ // ── Button variant: pill ───────────────────────────────────────────────
495
+ if (cs?.buttonVariant === 'pill') {
496
+ rules.push(`html[data-button-variant='pill'] [data-btn] {`);
497
+ rules.push(` border-radius: 9999px !important;`);
498
+ rules.push(`}`);
499
+ }
500
+ // ── Button variants: outlined / ghost ──────────────────────────────────
501
+ if (cs?.buttonVariant === 'outlined') {
502
+ rules.push(`html[data-button-variant='outlined'] [data-btn] {`);
503
+ rules.push(` background-color: transparent !important;`);
504
+ rules.push(` color: var(--primary) !important;`);
505
+ rules.push(` border: 1.5px solid var(--primary) !important;`);
506
+ rules.push(` box-shadow: none !important;`);
507
+ rules.push(`}`);
508
+ rules.push(`html[data-button-variant='outlined'] [data-btn]:hover {`);
509
+ rules.push(` background-color: color-mix(in srgb, var(--primary) 10%, transparent) !important;`);
510
+ rules.push(`}`);
511
+ } else if (cs?.buttonVariant === 'ghost') {
512
+ rules.push(`html[data-button-variant='ghost'] [data-btn] {`);
513
+ rules.push(` background-color: transparent !important;`);
514
+ rules.push(` color: var(--primary) !important;`);
515
+ rules.push(` border-color: transparent !important;`);
516
+ rules.push(` box-shadow: none !important;`);
517
+ rules.push(`}`);
518
+ rules.push(`html[data-button-variant='ghost'] [data-btn]:hover {`);
519
+ rules.push(` background-color: color-mix(in srgb, var(--primary) 10%, transparent) !important;`);
520
+ rules.push(`}`);
521
+ }
522
+ // ── Card hover effects ─────────────────────────────────────────────────
523
+ if (cs?.cardHoverEffect === 'lift') {
524
+ rules.push(`html[data-card-hover='lift'] [data-card]:hover,`);
525
+ rules.push(`html[data-card-hover='lift'] .glass-card:hover {`);
526
+ rules.push(` transform: translateY(-6px) !important;`);
527
+ rules.push(` box-shadow: 0 20px 40px rgba(0,0,0,0.12) !important;`);
528
+ rules.push(`}`);
529
+ } else if (cs?.cardHoverEffect === 'scale') {
530
+ rules.push(`html[data-card-hover='scale'] [data-card]:hover,`);
531
+ rules.push(`html[data-card-hover='scale'] .glass-card:hover {`);
532
+ rules.push(` transform: scale(1.02) !important;`);
533
+ rules.push(`}`);
534
+ } else if (cs?.cardHoverEffect === 'glow') {
535
+ rules.push(`html[data-card-hover='glow'] [data-card]:hover,`);
536
+ rules.push(`html[data-card-hover='glow'] .glass-card:hover {`);
537
+ rules.push(` box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 25%, transparent), 0 8px 32px rgba(0,0,0,0.1) !important;`);
538
+ rules.push(`}`);
539
+ } else if (cs?.cardHoverEffect === 'tilt') {
540
+ rules.push(`html[data-card-hover='tilt'] [data-card],`);
541
+ rules.push(`html[data-card-hover='tilt'] .glass-card {`);
542
+ rules.push(` transition: transform 200ms ease !important;`);
543
+ rules.push(`}`);
544
+ rules.push(`html[data-card-hover='tilt'] [data-card]:hover,`);
545
+ rules.push(`html[data-card-hover='tilt'] .glass-card:hover {`);
546
+ rules.push(` transform: perspective(1000px) rotateX(2deg) rotateY(-2deg) !important;`);
547
+ rules.push(`}`);
548
+ }
549
+ // ── Button variant: gradient ───────────────────────────────────────────
550
+ if (cs?.buttonVariant === 'gradient') {
551
+ rules.push(`html[data-button-variant='gradient'] [data-btn] {`);
552
+ rules.push(` background-image: var(--gradient-brand) !important;`);
553
+ rules.push(` background-color: transparent !important;`);
554
+ rules.push(` color: var(--primary-foreground) !important;`);
555
+ rules.push(` border: none !important;`);
556
+ rules.push(`}`);
557
+ }
558
+ // ── Button sizes ────────────────────────────────────────────────────────
559
+ if (cs?.buttonSize) {
560
+ const sizeMap = {
561
+ small: {
562
+ py: '0.375rem',
563
+ px: '0.75rem',
564
+ fs: '0.8125rem'
565
+ },
566
+ medium: {
567
+ py: '0.5rem',
568
+ px: '1rem',
569
+ fs: '0.875rem'
570
+ },
571
+ large: {
572
+ py: '0.625rem',
573
+ px: '1.5rem',
574
+ fs: '1rem'
575
+ },
576
+ xl: {
577
+ py: '0.875rem',
578
+ px: '2rem',
579
+ fs: '1.125rem'
580
+ }
581
+ };
582
+ const s = sizeMap[cs.buttonSize] ?? sizeMap.medium;
583
+ rules.push(`html[data-button-size='${cs.buttonSize}'] [data-btn] {`);
584
+ rules.push(` padding: ${s.py} ${s.px} !important;`);
585
+ rules.push(` font-size: ${s.fs} !important;`);
586
+ rules.push(`}`);
587
+ }
588
+ // ── Image styles ───────────────────────────────────────────────────────
589
+ // Target both [data-img] attributes and the .themed-image helper class.
590
+ if (cs?.imageStyle && cs.imageStyle !== 'default') {
591
+ const style = cs.imageStyle;
592
+ const sel = `html[data-image-style='${style}'] [data-img], html[data-image-style='${style}'] .themed-image`;
593
+ const hoverSel = `html[data-image-style='${style}'] [data-img]:hover, html[data-image-style='${style}'] .themed-image:hover`;
594
+ if (style === 'rounded') {
595
+ rules.push(`${sel} { border-radius: var(--radius-large, 1rem) !important; overflow: hidden; }`);
596
+ } else if (style === 'circle') {
597
+ rules.push(`${sel} { border-radius: 9999px !important; aspect-ratio: 1 / 1; object-fit: cover; }`);
598
+ } else if (style === 'grayscale') {
599
+ rules.push(`${sel} { filter: grayscale(1); transition: filter 300ms ease; }`);
600
+ rules.push(`${hoverSel} { filter: grayscale(0); }`);
601
+ } else if (style === 'duotone') {
602
+ rules.push(`${sel} { filter: grayscale(1) contrast(1.05); mix-blend-mode: luminosity; }`);
603
+ } else if (style === 'vignette') {
604
+ rules.push(`${sel} { box-shadow: inset 0 0 60px rgba(0,0,0,0.45); }`);
605
+ } else if (style === 'polaroid') {
606
+ rules.push(`${sel} { background: #fff; padding: 10px 10px 36px; box-shadow: 0 6px 16px rgba(0,0,0,0.18); border-radius: 2px; }`);
607
+ }
608
+ }
609
+ // ── Link styles ─────────────────────────────────────────────────────────
610
+ if (cs?.linkStyle) {
611
+ const style = cs.linkStyle;
612
+ const sel = `html[data-link-style='${style}'] [data-link], html[data-link-style='${style}'] .themed-link`;
613
+ const hoverSel = `html[data-link-style='${style}'] [data-link]:hover, html[data-link-style='${style}'] .themed-link:hover`;
614
+ if (style === 'underline') {
615
+ rules.push(`${sel} { text-decoration: underline; text-underline-offset: 0.15em; }`);
616
+ } else if (style === 'none') {
617
+ rules.push(`${sel} { text-decoration: none; }`);
618
+ } else if (style === 'underline-hover') {
619
+ rules.push(`${sel} { text-decoration: none; }`);
620
+ rules.push(`${hoverSel} { text-decoration: underline; text-underline-offset: 0.15em; }`);
621
+ } else if (style === 'highlight') {
622
+ rules.push(`${sel} { text-decoration: none; background-image: linear-gradient(transparent 60%, color-mix(in srgb, var(--primary) 28%, transparent) 0); }`);
623
+ } else if (style === 'animated') {
624
+ rules.push(`${sel} { text-decoration: none; background-image: var(--gradient-brand); background-size: 0% 2px; background-position: 0 100%; background-repeat: no-repeat; transition: background-size 300ms ease; }`);
625
+ rules.push(`${hoverSel} { background-size: 100% 2px; }`);
626
+ }
627
+ }
628
+ // ── Footer styles ─────────────────────────────────────────────────────────
629
+ if (cs?.footerStyle === 'dark') {
630
+ rules.push(`html[data-footer-style='dark'] footer { background-color: var(--foreground) !important; color: var(--background) !important; }`);
631
+ } else if (cs?.footerStyle === 'full-color') {
632
+ rules.push(`html[data-footer-style='full-color'] footer { background-color: var(--primary) !important; color: var(--primary-foreground) !important; }`);
633
+ } else if (cs?.footerStyle === 'gradient-top') {
634
+ rules.push(`html[data-footer-style='gradient-top'] footer { border-top: 3px solid transparent; border-image: var(--gradient-brand) 1; }`);
635
+ } else if (cs?.footerStyle === 'minimal') {
636
+ rules.push(`html[data-footer-style='minimal'] footer { background: transparent !important; border-top: 1px solid var(--border) !important; box-shadow: none !important; }`);
637
+ }
638
+ // ── Hover animations off — neutralise interactive transforms ──────────────
639
+ if (cs?.enableHoverAnimations === false) {
640
+ rules.push(`html[data-hover-animations='off'] [data-card]:hover, html[data-hover-animations='off'] .glass-card:hover, html[data-hover-animations='off'] [data-btn]:hover { transform: none !important; }`);
641
+ }
174
642
  }
175
643
  function applyDarkModeOverrides(rules, config, customization, selector) {
176
644
  if (config.colorMode !== 'dark' && config.colorMode !== 'auto') return;