# Form & Modal CSS Delivery Fix

## Problem

Template `style.php` files render CSS that is scoped to a specific modal-window HTML ID
(e.g. `#modalWindowGnCta5`). Before 4.0.41/4.0.42, this CSS was always emitted inline,
which:

1. Blocked first-paint — inline CSS in `<body>` disrupts rendering.
2. Polluted page markup — AI crawlers and SEO tools count inline scripts/styles
   as noise.
3. Prevented browser caching — a new inline block on every page load.
4. Duplicated CSS — if the same CTA appeared on multiple pages, identical CSS was
   re-emitted each time (never cached between page loads).

For form-only modals (widget-based, no CTA post), a second issue existed: the default
template's CSS was accumulated into `$GENOO_STYLES` and also pushed through a
`GenooCSS.add()` JavaScript call — causing the CSS to be applied twice (once directly,
once via script).

---

## Track A — Completed (4.0.41 + earlier)

### What was fixed

**Form style injection (`GenooCSS.add()` removal)**

The `[genoo_form]` shortcode and Form Widget were appending form theme CSS via:

```javascript
GenooCSS.add('#some-selector', 'color:#fff; ...');
```

This required JavaScript to run before the CSS was applied, and put CSS strings inside
script tags in the page body. Fixed by accumulating all form theme CSS into the
`$GENOO_STYLES` PHP global and emitting it once as a single `<style>` block in
`wp_footer` priority 1, before any modal HTML.

**Per-CTA modal CSS file caching**

`TemplateStorage::prepareTemplateForModalWindow()` was modified to:

1. Call `Generator::writeCtaCss($post->ID, $css)` after rendering style.php.
   - Writes `wp-content/uploads/genoo-css/cta-{id}.css` on first render.
   - File is never overwritten until invalidated.
2. Before emitting inline `<style>`, check whether the external file was already
   enqueued (`wp_style_is('genoo-cta-{id}', 'enqueued|done')`). If so, skip inline
   output entirely.
3. `Frontend::enqueue()` and `Frontend::footerFirst()` call `wp_enqueue_style()` for
   every `cta-{id}.css` file that exists, before the modal HTML renders.

**Cache invalidation:** `save_post_cta` deletes the CSS file, forcing regeneration on the
next page load.

**Per-form-modal CSS file caching**

`TemplateStorage::prepareTemplateForModalWindowForm()` was modified identically:

1. Call `Generator::writeFormModalCss($formId, $prefix, $css)`.
   - Cache key = `formId` + 8-char MD5 hash of the modal prefix string, because two
     different widget placements of the same form use different modal IDs and therefore
     different CSS selectors.
   - Writes `wp-content/uploads/genoo-css/form-{formId}-modal-{hash8}.css`.
2. Checks `wp_style_is` before emitting inline CSS.
3. `Frontend::footerFirst()` enqueues and immediately prints (`wp_print_styles`) each
   form-modal CSS file before the modal HTML renders at footer priority 999.

**Cache invalidation:** `customize_save_after` deletes all `form-*-modal-*.css` files,
because the default template CSS depends on Customizer settings (hide-required-label,
hide-modal-title, hide-modal-desc).

### State after Track A

| CSS type | Delivery | Cache key | Invalidation |
|----------|----------|-----------|--------------|
| Form theme CSS (wpme-styles CPT) | External file (`form-themes.css`) | — | `save_post_wpme-styles` |
| CTA modal CSS | External file (`cta-{id}.css`) | CTA post ID | `save_post_cta` |
| Form-only modal CSS | External file (`form-{formId}-modal-{hash8}.css`) | Form ID + widget modal prefix | `customize_save_after` |
| Customizer live preview | Inline `<style>` (unchanged) | — | — |

Remaining problem: each form-only modal widget placement generates a **separate CSS
file** even when two placements of the same form produce identical CSS. The file differs
only in the modal-window ID selector (`$id`). With N widget placements of M different
forms, there are up to N×M CSS files.

---

## Track B — Template Static CSS Extraction (4.0.43)

### Goal

Extract the **structural (non-color) CSS** portion of each template's `style.php` into
a shared, per-template CSS file that is served once regardless of how many CTA or form
instances use that template. The per-CTA / per-form dynamic color CSS remains in
instance-specific files.

### Approach

A **static rendering mode** is added to every `style.php` via a global flag
(`$GLOBALS['WPME_STATIC_CSS_MODE']`).

When the flag is set:

- `$id` is replaced with `.gn-custom-modal-{templateId}` (a CSS *class* selector rather
  than an HTML ID selector).
- All blocks that call `$view->getValueOf()` or `$view->getBinderSelectorOfPrefixedWith()`
  — and all PHP conditional blocks whose output depends on Customizer values — are
  **suppressed**.

The result is a self-contained CSS file with only the structural/layout rules, scoped
to the template's shared class. This file is identical for all CTAs/forms that use the
same template and can be cached by the browser across pages.

### Template analysis

| Template | Dynamic values | Notes |
|----------|----------------|-------|
| `default` | Conditionals: `gn-hide-required-label`, `gn-hide-modal-title`, `gn-hide-modal-desc` | No color values. All dynamic output is PHP conditionals. Clean split. |
| `full-screen` | Generated section (lines 265–302): background-image, text-color, background-color, submit-text-color, submit-background-color | Clean static / Generated split. |
| `sign-up-with-us` | Generated section (lines 199–234): same four color bindings + background-image | Clean static / Generated split. |
| `join-us-side-image` | Generated section (lines 174–278): submit colors, field hover/focus border, hide-required conditional | Clean static / Generated split. |
| `chevron-highlight` | `$textColor` and `$highlightColor` **inline inside structural rule blocks** (form * font-color, label color, button colors, .genooPopFull background) | Mixed — requires per-declaration wrapping. |
| `special-offer-badge` | Submit button colors and `.gn-btn label` color **inline inside structural rule blocks** | Mixed — requires per-declaration wrapping. |
| `horizontal-style` | Submit button colors, `.gn-btn label` color, modal height, `.genooForm` background-color, result-state colors **inline** | Mixed — requires per-declaration wrapping. |

### Changes to `style.php` files (all 7 templates)

Header section replaces:

```php
$id = isset($WPME_MODAL_ID) ? $WPME_MODAL_ID : '';
```

with:

```php
$staticMode = !empty($GLOBALS['WPME_STATIC_CSS_MODE']);
$id = $staticMode
    ? '.gn-custom-modal-' . basename(dirname(__FILE__))
    : (isset($WPME_MODAL_ID) ? $WPME_MODAL_ID : '');
```

**Clean-split templates** (`full-screen`, `sign-up-with-us`, `join-us-side-image`):
The entire Generated section is wrapped in `<?php if (!$staticMode): ?>`.

**`default`**: The three PHP conditional blocks (hide-required, hide-title, hide-desc)
are each wrapped in `<?php if (!$staticMode): ?>`.

**Mixed templates** (`chevron-highlight`, `special-offer-badge`, `horizontal-style`):
Individual dynamic property declarations (`color: <?= $view->getValueOf(...) ?>;`,
`background: ...`, etc.) are each wrapped in `<?php if (!$staticMode): ?>`. The
surrounding rule block remains; the property is simply absent in static mode.

### Changes to `Generator.php`

New methods for the template CSS file:

```php
public static function templateCssPath($templateId): string
public static function templateCssUrl($templateId): string
public static function templateCssExists($templateId): bool
public static function templateCssVersion($templateId): string
public static function templateCssHandle($templateId): string  // 'genoo-template-{id}'
public static function writeTemplateCss($templateId, string $css): bool  // write-once
public static function deleteTemplateCss($templateId): void
public static function deleteAllTemplateCss(): void
```

File naming: `wp-content/uploads/genoo-css/template-{templateId}.css`

Cache invalidation additions to `registerHooks()`:
- `customize_save_after` → also calls `deleteAllTemplateCss()` (default template
  depends on Customizer show/hide settings).
- `save_post_cta` → does NOT delete template CSS (template is not CTA-specific).

### Changes to `TemplateStorage.php`

New method `generateTemplateStaticCss($themeId)`:

1. Sets `$GLOBALS['WPME_STATIC_CSS_MODE'] = true`.
2. Sets `$GLOBALS['WPME_MODAL_ID'] = '.gn-custom-modal-' . $themeId`.
3. Calls `__getContentsOf($themeDir . $themeId . '/style.php')` via output buffering.
4. Unsets both globals.
5. Strips `<style>` wrapper tags.
6. Removes CSS rules where every property declaration is empty
   (`property: ;`) using a regex pass.
7. Returns the cleaned CSS string.

Updated `prepareTemplateForModalWindow()`:

After calling `Generator::writeCtaCss($post->ID, $themeCss)`:
```php
if (!Generator::templateCssExists($themeId)) {
    $staticCss = $this->generateTemplateStaticCss($themeId);
    Generator::writeTemplateCss($themeId, $staticCss);
}
```

Updated `prepareTemplateForModalWindowForm()`:

Same: writes `template-default.css` on first form-only modal render.

### Changes to `Frontend.php`

`enqueue()`: before existing per-CTA enqueuing loop, collect unique template IDs from
the `$footerCTAModals` data and enqueue each `template-{templateId}.css` file if it
exists.

`footerFirst()`: before existing per-CTA and per-form-modal enqueuing, collect template
IDs from shortcode CTAs and form widgets, enqueue their template CSS files.

### End state after Track B

| CSS type | Delivery | Cache scope | Invalidation |
|----------|----------|-------------|--------------|
| Form theme CSS | `form-themes.css` | Site-wide | `save_post_wpme-styles` |
| Template structural CSS | `template-{templateId}.css` | Per template (shared by all CTAs/forms using it) | `customize_save_after` |
| CTA color CSS | `cta-{id}.css` | Per CTA post | `save_post_cta` |
| Form-only modal CSS | `form-{formId}-modal-{hash8}.css` | Per form + widget placement | `customize_save_after` |

For `default` template form-only modals: the per-form-modal file is typically empty
(or contains only 1–3 show/hide rules) because all color output is suppressed in the
per-instance CSS generation once the template file exists. The browser loads
`template-default.css` once and caches it for all form widgets sitewide.
