TL;DR
- Built on Hostinger AI theme + WooCommerce, heavily modified with a custom CSS design system injected via the header template part
- The biggest technical challenge was WordPress block gap injection — inline margin-top:40px beats external CSS every time
- Every page uses the same design system: CSS variables, tp-page-hero class, tp-section / tp-section-dark alternating sections
Why document this?
This site was built entirely via WordPress REST API and browser tooling — no page builder, no Elementor, no Beaver Builder. Every section, every dark card, every full-width hero was hand-crafted in HTML injected via wp:html blocks. This post documents the approach, the system, and the things that broke along the way.
The design system
Rather than scattering custom CSS across pages, every style lives in the header template part (hostinger-ai-theme//header). This file contains the full design system as a <style> block:
:root {
--tp-bg: #05070D; /* primary dark background */
--tp-bg2: #0B1020; /* secondary dark — cards, panels */
--tp-cyan: #22d3ee; /* primary accent */
--tp-blue: #0693e3; /* secondary accent */
--tp-violet: #9b51e0; /* tertiary accent */
--tp-grad: linear-gradient(135deg, var(--tp-cyan), var(--tp-blue), var(--tp-violet));
--tp-display: 'Space Grotesk', sans-serif;
}
/* Full-width section classes */
.tp-section-dark {
background: var(--tp-bg2);
box-shadow: 0 0 0 9999px var(--tp-bg2);
clip-path: inset(0 -9999px);
padding: 80px 24px;
}
.tp-section {
background: var(--tp-bg);
box-shadow: 0 0 0 9999px var(--tp-bg);
clip-path: inset(0 -9999px);
padding: 80px 24px;
}
The box-shadow + clip-path trick is how you make a div appear full-width inside a constrained content column without changing the WordPress template structure.
The block gap problem
WordPress’s is-layout-constrained block system injects margin-top: 40px as an inline style on every block. Inline styles beat external CSS — so the gap between sections appeared despite my CSS rules explicitly setting margin-top: 0.
The fix: add margin-top: 0 directly to the element’s own inline style attribute in the HTML block. This sits at the same specificity level as the WordPress injection and wins.
<!-- WRONG: margin-top:0 in external CSS — loses to WordPress inline -->
.page-id-18 .entry-content > div:first-child { margin-top: 0 !important; }
<!-- RIGHT: margin-top:0 directly on the element -->
<div style="background:var(--tp-bg);...;margin-top:0;margin-bottom:0">
The hero pattern
Every page uses the same hero structure via the tp-page-hero class, which is styled in the header CSS:
.tp-page-hero {
background: var(--tp-bg);
box-shadow: 0 0 0 9999px var(--tp-bg);
clip-path: inset(0 -9999px);
text-align: center;
padding: 100px 24px 80px;
}
.tp-page-hero h1 {
font-family: var(--tp-display);
font-size: 56px;
font-weight: 800;
color: #fff;
letter-spacing: -1.5px;
line-height: 1.1;
}
WooCommerce dark skin
WooCommerce renders its own stylesheets that are extremely difficult to override from external CSS — particularly the login form and My Account page. The solution was injecting a <style> block directly in the page content with body.page-id-58 specificity. Page-level inline styles load after theme stylesheets and win the cascade.
Things that broke
- Page titles showing as black text — the Hostinger theme renders a
<h1 class="hostinger-ai-page-title">that its own stylesheet sets to black. Fix: target it explicitly withbody.page-id-X h1 { color: #fff !important; } - Footer showing blue on dark pages — the footer uses a separate template part with its own background. Can’t be changed without editing the footer template — so it’s intentionally left as a contrasting section
- LiteSpeed cache serving stale CSS — every CSS change needs a full cache purge before you can verify it visually. Built the habit of purging before every verification screenshot
- WP REST API returning stale content — occasionally the API returns cached content. Wait 30 seconds or force a page refresh after saving
The REST API workflow
Every page on this site was built or modified using the WordPress REST API via wp.apiFetch() — available on any wp-admin tab. The pattern:
- Read current content:
wp.apiFetch({ path: '/wp/v2/pages/ID?context=edit' }) - Modify in JavaScript string manipulation
- Save:
wp.apiFetch({ path: '/wp/v2/pages/ID', method: 'POST', data: { content } }) - Purge LiteSpeed cache
- Verify visually
💡 The constraint of not using a page builder forced cleaner, more portable HTML. Every page is plain HTML in wp:html blocks — no shortcodes, no plugin dependencies, no vendor lock-in beyond WordPress itself.