How to build a reliable per-article typography system on WordPress when Astra sets global heading sizes, Elementor manages CSS output, and LiteSpeed UCSS strips scoped stylesheet rules.
Most WordPress setups using Astra + Elementor + LiteSpeed Cache have this invisible conflict:
h2 = 90px)The result: your override CSS is correctly saved, passes every check, and is invisible to the browser.
⬡Lab confirmation
This behaviour is confirmed in the LiteSpeed UCSS stripping research. The UCSS file for asquaresolution.com showed 0 occurrences of .postid-8717 despite the Kit CSS being correctly saved.
Inline styles on heading elements are the only reliable approach on a LiteSpeed + Astra + Elementor stack.
| Approach | Works? | Why |
|---|---|---|
| Astra Customizer global size | ✓ | Applied globally, not scoped |
Elementor Kit custom_css | ✗ | UCSS strips scoped rules |
<style> block in post content | ✗ | wpautop corrupts it |
Inline style= on element | ✓ | In HTML, immune to UCSS |
!important in Kit CSS | ✗ | Rule isn't in UCSS — irrelevant |
The second failure mode — wpautop — is important to understand separately.
WordPress's the_content filter runs wpautop() on post content before rendering. This function:
<p> tags<style> elementsCSS written with blank lines between rules looks, to wpautop, like separate paragraphs. It injects <p> and </p> inside the style block. Confirmed: 7 <p> injections in post 8717's corrupted style block.
<!-- What you wrote -->
<style type="text/css">
.postid-8717 h2 { font-size: 30px; }
.postid-8717 h3 { font-size: 20px; }
</style>
<!-- What WordPress rendered -->
<style type="text/css">
<p>.postid-8717 h2 { font-size: 30px; }</p>
<p>.postid-8717 h3 { font-size: 20px; }</p>
</style>
The CSS is broken. Compact CSS (no blank lines) reduces but does not eliminate this problem on some configurations.
Applied directly to HTML elements via style= attribute in post content:
<!-- Section H2 -->
<h2 style="font-size:28px;line-height:1.28;font-weight:600;letter-spacing:-0.2px;margin:36px 0 12px;">
Heading Text
</h2>
<!-- Section H3 -->
<h3 style="font-size:19px;line-height:1.32;font-weight:600;margin:24px 0 10px;">
Sub-heading
</h3>
<!-- Hero H2 on dark background -->
<h2 style="font-size:42px;line-height:1.15;font-weight:800;letter-spacing:-1px;margin:0 0 10px;color:#ffffff;">
Hero
</h2>
| Property | Value | Reasoning |
|---|---|---|
font-size H2 | 28px | Editorial scale; readable at 1140px container; doesn't wrap excessively |
font-size H3 | 19px | Clear tier below H2 without too much compression |
line-height H2 | 1.28 | Tighter than body (1.65em) for headings; consistent with modern editorial |
font-weight | 600 | Lexend at 600 reads as bold without appearing heavy at 28px |
letter-spacing | -0.2px | Negative tracking at larger sizes matches editorial convention |
margin | 36px 0 12px | Generous top space establishes section break; tight bottom keeps heading close to content |
LiteSpeed UCSS also strips @media rules that it considers unused. Use clamp() in inline styles for responsive values without CSS:
<!-- Responsive padding on a dark container block -->
<div style="padding:clamp(20px,4vw,45px);">
This eliminates the need for a media query while providing responsive behaviour:
4vw = 15px → clamped to 20px4vw = 30.7px → uses calculated value4vw = 48px → clamped to 45pxFor batch application to existing posts:
import urllib.request, base64, json
WP_BASE = "https://your-site.com/wp-json/wp/v2"
AUTH = "Basic " + base64.b64encode(b"user:app_password").decode()
H2_STYLE = "font-size:28px;line-height:1.28;font-weight:600;letter-spacing:-0.2px;margin:36px 0 12px;"
BARE_H2 = '<h2 class="elementor-heading-title elementor-size-default">'
STYLED_H2 = f'<h2 class="elementor-heading-title elementor-size-default" style="{H2_STYLE}">'
def get_post(post_id):
req = urllib.request.Request(
f"{WP_BASE}/posts/{post_id}?context=edit",
headers={"Authorization": AUTH}
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
def patch_post(post_id, content):
body = json.dumps({"content": content}).encode()
req = urllib.request.Request(
f"{WP_BASE}/posts/{post_id}",
data=body, method="POST",
headers={"Authorization": AUTH, "Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())
post = get_post(8717)
content = post["content"]["raw"]
new_content = content.replace(BARE_H2, STYLED_H2)
# Verify before applying
assert STYLED_H2 in new_content
assert BARE_H2 not in new_content
patch_post(8717, new_content)
All of the above is necessary only because Astra's Customizer has landing-page heading sizes set globally. The correct long-term fix:
Appearance → Customize → Typography → Headings → H2 → set to 28–32px
This changes the global default. Every article benefits automatically. The inline style work becomes unnecessary for new articles, and the 442+ existing articles with rendering issues are fixed in one action.
The inline style system documented here is the correct workaround for sites where the Customizer setting cannot be changed immediately.