IPRA × GrowthZone × Beaver Builder — Build Field Guide
Hard-won notes from rebuilding the Regions page. Read the "Golden Rules" first; the rest is detail + copy-paste patterns. This environment has several non-obvious traps — most of our wasted time came from not knowing them.
Environment: GrowthZone CMS → WordPress → Beaver Builder + PowerPack add-on. WCAG 2.1 AA required (no AudioEye violations). IPRA Blue:
#009abc/#007191. Font: Raleway. Icons: Font Awesome 5 ("Font Awesome 5 Free", weight 900).
⭐ Golden Rules (the TL;DR that saves hours)
- Decide where CSS goes by reuse (this matters a lot here):
- Single page / single row of styling → that ROW's Custom CSS field (PowerPack, Row → Advanced). It renders live in the canvas, bundles with the row when you template it, auto-scopes to the row, and — critically — likely dodges the Customizer sanitizer (so
>,:has(), and FA glyph escapes tend to work there). This is the better default for page-specific work and would have prevented most of our pain. - Shared across multiple pages / truly page-wide → Customize → Additional CSS, scoped to a page-only class (e.g.
.regions-page-section). Global + live, but watch the sanitizer (rule 2).
- Single page / single row of styling → that ROW's Custom CSS field (PowerPack, Row → Advanced). It renders live in the canvas, bundles with the row when you template it, auto-scopes to the row, and — critically — likely dodges the Customizer sanitizer (so
- The Customizer's Additional CSS sanitizer mangles things (the row Custom CSS field generally does NOT). When writing in Additional CSS, AVOID:
>child combinators → use descendant selectors instead (.a .b, not.a > .b).:has()→ it can invalidate the entire rule block. Don't use it there.- Font Awesome glyph escapes
content:"\fXXX"→ get eaten → use an embedded SVG for icons instead.
- Native modules for content, HTML modules only for code. Heading / Text Editor / Button for anything an editor will touch. HTML module only for the interactive map + its JS.
- Top-level rows only. Nested rows are unreliable in this environment.
- Judge layout in Preview (P), never the editing canvas. The canvas renders a different DOM than the live page and is not WYSIWYG for custom CSS.
- Select modules from the Outline panel (list icon, top-right), not by hovering the canvas. This is the teachable workflow for non-technical successors.
- After any CSS change: Publish, then hard-refresh (Ctrl+Shift+R). GrowthZone caches CSS aggressively.
Where CSS/JS can and can't go
| Location | Page-wide? | Live-renders in canvas? | Notes |
|---|---|---|---|
| Customize → Additional CSS ✅ | Yes | Yes | Primary home for page CSS. Sanitizer caveats above. |
| BB Tools → "Layout CSS & JavaScript" | — | — | DISABLED in this build (Tools menu jumps History → Global Styles). Not available. |
Row → Custom CSS field (bb_css_code, PowerPack) |
Scoped to that row | Yes (live) | Best for single-page / single-row styling. Auto-scopes to the row; likely dodges the sanitizer (>, :has(), FA escapes tend to work); bundles with the row when templated. Not for cross-page sharing. |
HTML module <style> (module main content field) |
Yes | No (stale until reload) | Works but the module is invisible & canvas won't refresh it live. Use only when CSS isn't an option (it always is here). |
| HTML module — the module's separate "CSS" field | n/a | n/a | ⚠️ TRAP: strips/scopes <style>, module renders empty. Never put <style> here. |
JS (e.g. the interactive map script) has no panel either → it lives in the HTML module's main content field alongside the map markup. Map JS does not run in the editing canvas — only in Preview/live.
Where to put CSS — decision
| Situation | Put it in | Why |
|---|---|---|
| Styling used on one page / one row | That Row's Custom CSS field | Live in canvas, bundles with the row when templated, dodges the Customizer sanitizer (so >/:has()/FA escapes work), scoped automatically |
| Styling shared across pages or genuinely page-wide | Customize → Additional CSS (scoped by class) | One source of truth; but obey the sanitizer rules |
| A one-off tweak to a single element | That node's CSS field | Self-contained, travels with the node |
Default for page-specific work: the Row Custom CSS field. It likely would have prevented most of the Regions-page saga (the sanitizer eating >, :has(), and the FA glyph). Target the class (.ipra-card, etc.) from the row field — one rule covers all cards in the row; don't write per-individual-card CSS.
⚠️ Verify scoping once per new page: PowerPack node CSS auto-scopes to that node (it prepends the row's selector, or exposes a selector/node keyword). Drop a throwaway rule (e.g. a red border on .ipra-card) and confirm it (a) lands and (b) only affects that row, before building on it. The layout gremlins below (.fl-col-group::before stealing grid cells, .fl-col-small stacking) still apply wherever the CSS lives — but the fixes work fine in the row field.
Page skeleton (Regions example)
Top-level rows, each <section>, class regions-page-section + a row modifier:
| Row | Width | Class | ID |
|---|---|---|---|
| Hero | Full Width | regions-page-section regions-hero |
regions-hero-row |
| Map | Fixed | regions-page-section regions-map |
main-content (skip-link target) |
| Cards | Fixed | regions-page-section regions-cards |
— |
| CTA | Fixed | regions-page-section regions-cta |
— |
Add class/ID/tag via Row → Advanced → HTML Element. Rename a node via right-click → Rename (or Outline).
The card-grid saga → the pattern that works
Goal: a 2×2 card grid from a single 4-column row. What we learned:
- BB tags narrow columns
.fl-col-smalland force-stacks themwidth:100% !importantwith high specificity. Low-specificity overrides lose. - The group is
.fl-col-groupin the DOM (PowerPack doesn't actually rename it — earlier failures were the:has()/>sanitizer issue, not the class). - CSS Grid is the winning model (no flex wrap-math to overflow). BUT:
- BB's
.fl-col-group::before/::afterclearfix pseudo-elements become grid cells and shove every card off by one (classic "first cell empty, cards brick"). You must hide them.
Proven card layout (descendant selectors only, no > outside what survives, high specificity to beat .fl-col-small):
/* 2-column grid, equal-height */
.regions-cards .fl-col-group{display:grid !important;grid-template-columns:1fr 1fr !important;gap:26px !important;align-items:stretch !important;}
.regions-cards .fl-col-group::before,.regions-cards .fl-col-group::after{display:none !important;} /* <-- the fix for bricking */
.regions-page-section.regions-cards .fl-col-group .fl-col.ipra-card{
width:auto !important;max-width:none !important;min-width:0 !important;
flex:none !important;float:none !important;clear:none !important;
}
/* internal padding — descendant, NOT ".ipra-card > .fl-col-content" */
.regions-page-section.regions-cards .ipra-card .fl-col-content{padding:34px 36px !important;}
@media(max-width:900px){
.regions-cards .fl-col-group{grid-template-columns:1fr !important;}
}
Card module class goes on the column (e.g. ipra-card ipra-card--teal). Card background/border/shadow on .ipra-card. Remember: cards stack to 1 column under 900px by design — a narrow browser window (or the Claude side panel open) will show the stack; judge at full desktop width.
Icons without Font Awesome (sanitizer eats the glyph)
The site runs FA5, but content:"\f3c5" gets stripped by the Customizer, so the icon square renders empty. Use an embedded, URL-encoded SVG as a background (encode <→%3C, >→%3E, "→%22, #→%23). Set the colored square with background-color in variants (not the background shorthand, which would wipe the SVG):
.ipra-card h3::before{content:"";width:48px;height:48px;border-radius:14px;flex-shrink:0;pointer-events:none;
background-color:#009abc;background-repeat:no-repeat;background-position:center;background-size:20px 20px;
background-image:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 384 512%22%3E%3Cpath fill=%22%23ffffff%22 d=%22M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z%22/%3E%3C/svg%3E');}
.ipra-card--lime h3::before{background-color:#5a8e22;}
(That path is FA map-marker-alt. Swap the d= path for other icons.)
Native module gotchas
- Eyebrow / label text: keep it formatted as Paragraph in the Text Editor, not "Heading 2" — a stray heading format changes the tag and breaks
.eyebrow pselectors. Style robustly with.eyebrow, .eyebrow *{...!important}. - Headings: BB wraps Heading text in
<span class="fl-heading-text">; the theme styles that span with high specificity. Target.row .fl-module-heading .fl-heading-text(and add!important) to win. - County / tag lists as pills: type the list in a Text Editor's "Text" (HTML) tab as
<ul><li>…</li></ul>(the Visual tab dumps everything into one bullet). Then CSS turnsliinto pills:.ipra-card-counties ul{display:flex;flex-wrap:wrap;gap:6px;list-style:none;margin:0;padding:0;} .ipra-card-counties li{background:#f8fafc;border:1px solid #e2e8f0;border-radius:20px;padding:4px 12px;font-size:12px;font-weight:600;color:#009abc;display:inline-flex;} - "Representative:" bold/colored: must be wrapped in
<strong>in the editor (select word → Bold) for.rep strong{color:…}to apply.
Editor-friendliness (for whoever inherits the page)
Decorative CSS fights the editing canvas. BB adds class fl-builder-edit to the page while editing (the site already relies on body:not(.fl-builder-edit) patterns) — use it to switch effects off in the editor only:
/* EDITOR OVERRIDES — Beaver Builder canvas only */
.fl-builder-edit .regions-hero{overflow:visible !important;}
.fl-builder-edit .regions-hero::before,.fl-builder-edit .regions-hero::after{display:none !important;}
.fl-builder-edit .regions-hero .fl-row-content-wrap{z-index:auto !important;}
.fl-builder-edit .ipra-card{transition:none !important;}
.fl-builder-edit .ipra-card:hover{box-shadow:0 5px 15px rgba(0,0,0,.06) !important;}
Also:
- Put
pointer-events:noneon every decorative pseudo-element (background blobs, icon squares) so they don't block clicking the text/modules underneath. - Prefer shadow-only hover (no
transform:translateY) so cards don't dodge the cursor / shift BB's toolbar. - The hover "artifacting" (duplicated content under the toolbar) is BB's own hover overlay — editor-only, cosmetic, not fixable via CSS. Tell editors to use the Outline panel.
Diagnostics that actually settle arguments
Run in DevTools Console on the live/preview page (not the editor — the editor lives in #fl-builder-ui-iframe):
// What wraps the cards, and is it laying out as expected?
let c=document.querySelector('.ipra-card');let p=c.parentElement;
console.log('CARD width:',getComputedStyle(c).width);
console.log('PARENT display:',getComputedStyle(p).display,'| class:',p.className);
// What Font Awesome family is actually loaded?
let i=document.querySelector('i.fas,i.fa,.fl-icon i');console.log(i&&getComputedStyle(i).fontFamily);
Rule of thumb: if a cosmetic style applies but a layout/>/:has()/content rule doesn't, suspect the sanitizer, not specificity.
Build fast: duplicate + Save As templates
Don't rebuild repeating elements (cards, callouts, section shells) from scratch.
- Node toolbar (hover any row/column/module) icons L→R: move · settings (wrench) · duplicate · save as (disk) · delete.
- Build one, duplicate it, edit text. Right-click → Duplicate (or the toolbar icon) clones the node with its classes — far faster than rebuilding the module stack each time.
- Save As a reusable template: settings dialog → "Save As..." (bottom), or the toolbar disk icon, or right-click → Save As. Name it (e.g. "IPRA Region Card"). Reuse via + Add Content → "Saved" tab (Saved Rows / Columns / Modules) — available on every page, site-wide.
- ⚠️ Global vs not: "Save as Global" (checked) links all instances to one master — editing one changes them all. Leave it unchecked for things you customize per-use (like cards); check it only for truly identical site-wide blocks.
- Templates carry markup + classes, not the stylesheet. Keep visual/layout CSS centralized by class in Additional CSS; the saved item picks it up automatically wherever that CSS is present.
- Recommended card workflow: build one card column → duplicate ×3 → edit text → (optionally) Save As the whole row for other pages.
Fast path for a new page
- Build rows top-level, native modules for content, one HTML module for any interactive/code block.
- Add classes:
<page>-page-sectionon each row + a row modifier; a clear class on every styled module. - Drop the page CSS into Customize → Additional CSS, scoped to those classes. Start
@import(fonts) at the very top. - Use CSS Grid for multi-card layouts; hide
.fl-col-group::before/::after; beat.fl-col-smallwith high-specificity card widths. - No
>, no:has(), no FA glyph escapes — descendant selectors + SVG icons. - Add the editor-overrides block +
pointer-events:noneon decorations. - Publish (Customizer and BB layout) → hard-refresh → verify in Preview at full width + mobile.
WCAG quick-keeps
- Skip link to
#main-content(put that ID on the first content row). - Visible focus:
outline:3px solid #e2cc00on links/buttons. @media (prefers-reduced-motion: reduce)to kill animations.- Contrast: stick to the IPRA teal/navy on white; white text only on
#007191/#005f7aor darker.