| name | ethora-theme |
|---|---|
| description | Use for ANY UI / frontend / layout work in the Ethora WordPress theme (ethora-theme) — building new marketing pages, redesigning sections, editing page-*.php templates or template-parts, or adding components. Enforces the theme's non-negotiable design rules (design tokens only, max 1200px content width, Open Sans only, brand blue #0052CD, strict 16px spacing scale) and provides a catalog of ready-made, reusable section blocks with screenshots and copy-paste usage. Invoke BEFORE writing or editing any markup/CSS in this theme. |
This is the design contract for the ethora-theme WordPress theme. If you follow it, you cannot drift outside the brand. Two things govern every change:
- The rules — colour, type, spacing, radius, width. Source of truth is
css/tokens.css(the machine source) andDESIGN.md(the human guide). A condensed version is below. - The blocks — ready-made, reusable sections in
template-parts/. Reuse these instead of inventing new layouts. Full catalog with screenshots + props:references/BLOCKS.md.
The reference implementation is
page-self-hosted-server.php— fully tokenised, uses every block below. Copy its patterns when building new pages.
Build only with var(--token). Never hardcode a hex colour or a px value for
colour / font / spacing / radius / width. If a value you need doesn't exist as a
token, add it to css/tokens.css first, then use it. This is the "no
ad-hoc drift" rule — it's what keeps every page on-brand.
A redesign restyles the page — it NEVER rewrites it. Every piece of user-facing text on the existing page is carried over verbatim: headings, leads, body paragraphs, list items, button labels, FAQ questions and answers, table cells, image-adjacent captions. You may style text (weight, size, colour, layout, which block it lives in) — you may not edit, rephrase, "improve", shorten, expand or translate it.
- A block needs a text prop the original page doesn't have (a
lead, a card blurb, a trust row)? Leave it empty / omit the prop — do NOT invent marketing copy. - Any new or replacement copy requires the user's explicit approval BEFORE it ships — propose it, don't write it in.
- Found broken copy (duplicated lines, paste errors, typos, lorem)? Flag it and ask — never silently "fix" the wording.
- The existing wording carries SEO intent — silently rewording a heading can lose rankings even when it "reads better".
When the task is creating a new page or redesigning an existing one (a whole page, not a
single section), confirm its frame with the user via AskUserQuestion BEFORE laying anything
out. Don't assume — ask:
-
Default layout, or custom? — In both cases the page ALWAYS uses our own header and footer:
get_header('v2')at the very top andget_footer('v2')at the very bottom. Never drop, replace or restyle the header/footer, whatever the answer. -
If DEFAULT layout → the page opens with the reusable hero block (
template-parts/section-hero.php). Ask which hero variant to use:- Light —
variant => ''(the default light-gradient hero, dark text), or - Blue —
variant => 'v2'(bright brand-blue gradient, white text; pair with white buttonslight+outline-light).
Both variants share the identical hero layout — only the colour changes (see the hero layout invariant in the rules). Then build the rest of the page with the usual defaults.
- Light —
-
If CUSTOM (not default) → ask the user which section should be the first one, then build from there.
-
Everything else keeps the current defaults (reuse the ready-made blocks, section vertical rhythm, reveal-on-scroll, the quality floor) — only these choices are gathered up front.
This intake is intentionally minimal for now and will be expanded later. Whatever the answers, every rule in this skill still applies — the intake never overrides the hard rules.
- Read
css/tokens.cssand skimDESIGN.md. - Before building a section, check the catalog (
references/BLOCKS.md). If a block fits, reuse it viaget_template_part()— don't rebuild it. - Build with tokens only. Match the surrounding code's idiom.
- Hit the quality floor (a11y / performance / responsive — below) and add the default reveal-on-scroll animation (see Motion — every new page/redesign ships it). Then count the cards in every grid (including the item-grids inside reusable blocks) and confirm no orphan last row — see Card grids below; it's the most-missed rule.
- New reusable block? Make it self-contained and add it to the catalog (see Adding a new block).
Full detail and the token table are in DESIGN.md. The hard ones:
- Tokens only. No literal hex/px for colour, type, spacing, radius, width.
- Copy is untouchable (GOLDEN RULE). Restyle, never rewrite: all existing user-facing text is carried over verbatim; empty props over invented copy; any new/changed wording needs the user's explicit approval first (see Copy is untouchable above).
- Colour: brand blue
--primary#0052CD+ grey does ~90%. Dark/CTA panels use--primary-dark#002398. Semantic--green/--orange/--red/--purpleonly for meaning, sparingly. One bold accent per screen; keep the rest quiet. - Type: Open Sans only — every
--font-*maps to it. Differentiate with weight/size, never a second typeface. Headings = heavier weight. - Spacing: strict 16px scale —
--space-16/32/48/64/80/96.--space-8is the ONLY sub-16 value (inline icon↔label/chip gaps). Never hand-type12/22/30px. Horizontal gutter is fixed 24px (--section-x). - Section vertical rhythm — collapse the seam between two non-blue sections (HARD).
When two consecutive full-width sections are both light (i.e. NOT a blue / full-bleed
brand band — white,
--surface-alt, or a soft gradient card on white), their y-padding must NOT stack. The seam between them is a singlevar(--section-y-sm)gap, never two paddings. Rule: a light section directly followed by another light section drops itspadding-bottomto0; the lower light section owns the gap viapadding-top: var(--section-y-sm). A light section keeps its fullpadding-bottomwhen the section below is a blue band (blue full-bleed bands always keep their own symmetric padding on both sides — the colour must breathe), and a light section directly after a blue band keeps its normal top padding. Implement it declaratively, not by hand-tuning each section: wrap ONLY the light sections in<div class="shs-sec">(leave blue bands unwrapped so a run never collapses across one), then in the page<style>:(scope the two selectors to the page's.shs .shs-sec:has(+ .shs-sec) > * { padding-bottom: 0 !important; } .shs .shs-sec + .shs-sec > * { padding-top: var(--section-y-sm) !important; }
<main>class). Reference:page-self-hosted-server.php. Exception — consecutive split cards sit tighter. When two neighbouring light sections are bothshs-split-section(the split-card block), the seam between them is a singlevar(--space-32), notvar(--section-y-sm)— they read as one connected series. Mark those wrappers.shs-sec.is-splitand add a higher-specificity override after the two rules above:Don't hand-tune the split cards'.shs .shs-sec.is-split + .shs-sec.is-split > * { padding-top: var(--space-32) !important; }
pad_top/pad_bottomargs to fake this — let the rule own it. Reference:page-healthcare.php. (If a section ships no native padding of its own — e.g..faq— give the last light section before a blue band an explicitpadding-bottom: var(--section-y-sm)so it doesn't butt against the band.) - Width (HARD): content never exceeds 1200px (
--container-xl). Every full-width section wraps at--content-max(1152) centred, with the section providing--section-xpadding — so all sections share the same edges and line up with the header. Never give a section a different max-width/gutter. Narrower centred columns may use--container-lg/md/smor--measure. - Radius:
--radius-*tokens; CTA buttons are--radius-btn(12px); cards--radius-2xl(18) / big cards--radius-3xl(24); chips/avatars--radius-pill. - Buttons (CTA): the brand CTAs (Get started / Book a Call) —
--radius-btn, Open Sans 600; primary =--primarybg + white (hover--primary-dark); outline =2px solid --primary+ primary text. Reuse the canonical classes — never restyle a button:.btn .btn-primary/.btn-outline/.btn-light/.btn-outline-light(global,css/index.css), or the token-based partial variants that follow the exact same rule (.shs-btn*insection-hero/section-cta-dark,.ppc-btn). No pill or other-radius CTAs. See Core UI primitives inreferences/BLOCKS.md. - Slider / nav (switch) buttons: prev/next for any carousel/slider use ONLY the brand
.slider-btnstandard (as in Our Case Studies) — 40px (2.5rem) square, radius--radius-btn(12px),1px solid --primaryborder, transparent bg,--primarychevron, hover →--primary-light, centred. Reuse the.slider-btnclass — never invent another nav-button style. - Toggle switch: any binary/segmented toggle (Monthly/Yearly, tabs) uses ONLY the brand
.ppc-toggle/.ppc-tgstandard (as in the pricing cards) — a--radius-pilltrack on--whitewith a--border, and the active segment filled--ink+ white text. Reuse this markup; never build a bespoke switch. See Core UI primitives inreferences/BLOCKS.md. - Brand-blue section background: the ONLY blue-fill background for a full-bleed section is
the token
--gradient-brand(brand-500 → brand-800, 135deg) — the statement band, cards carousel and stats band all use it. Never hand-write thatlinear-gradientinline; reference the token. (Dark/CTA panels are different — those use the.shs-darkimage treatment below.) - Never two blue/dark full-bleed bands back-to-back (HARD). Any blue/dark full-bleed section
(
--gradient-brandband,section-stats,section-split-cardwithdark: true,.shs-dark/section-cta-dark,section-trust-band) must ALWAYS be separated from the next one by a light section — the colour must breathe against white. Consecutive light sections are allowed (that's the vertical-rhythm rule above); consecutive blue/dark ones are NOT. In particular the closingsection-cta-darkmust be preceded by a light section — don't stack a blue statement band (split-card dark:true) directly on top of it; make that statement a light centred section instead (reference:page-case-study-drtalks.php/page-case-study-atom-advantage.php, the.cs-outcomeblock). The page hero gradient is the opener and doesn't count as a band for this rule. - Header & hero clearance: one header on every page — the light-blue gradient bar
(never plain white). The hero block (
section-hero) is full-viewport —min-height: 100vh, content vertically centred in the visible area below the fixed header (top paddingvar(--header-h)clears it — NOT--hero-pt, and no other vertical padding), and it grows instead of clipping when content is taller than the viewport. Any non-hero first section still clears the header withpadding-top: var(--hero-pt). - Hero layout invariant (HARD): every hero — light or the blue
variant => 'v2'— has the identical block layout: text column (eyebrow → h1 → lead → buttons → trust) left, media framed in the right column and vertically centred, compliance strip full-width below (label left, items right). A variant only recolours (background / text / border / opacity) — it must never change the layout, pin or viewport-size the media (which makes it cover the screen / go off-centre), or restyle the compliance strip. Never add page-level CSS to "fix" the hero. If a hero looks wrong, fixtemplate-parts/section-hero.phpso every page benefits; a page only ever passes props. Reference:page-self-hosted-server.php(light) andpage-npm-chat-component.php(v2) render the same layout. - Hero composition — Book a Call, trust row, floating badges (HARD). Every hero ships all three:
- Two buttons, Book a Call rightmost. The page's own CTA (its original label/URL verbatim)
sits on the LEFT;
Book a Call('modal' => true) is ALWAYS present and ALWAYS the rightmost button. Default pairing on the light hero: page CTAoutline+ Book a Callprimary; on the bluev2hero use the white pair (outline-light+light). Every page with a modal button also includestemplate-parts/section-book-call-modal. - A green-check
trustrow under the buttons — 3 items, minimal wording (2–4 words each), matching the page's context; reuse claims already on the page verbatim when they fit. - Two floating
badgesover the hero media (badgesprop → the.ehero-badgechips: 1st bottom-left, 2nd'pos' => 'tr'top-right) — each a shorttitle+ one-linetextthat reflect the page's content, plus a thematic line icon (17px,stroke="currentColor"). This rule is the user's standing, explicit approval for adding the Book a Call label, the trust items and the badge text on redesigns — the Copy is untouchable rule still governs every other word. Reference:page-self-hosted-server.php.
- Two buttons, Book a Call rightmost. The page's own CTA (its original label/URL verbatim)
sits on the LEFT;
- Dark panels: ALWAYS the brand
.shs-darktreatment (--primary-dark~85% overimages/start-free.png) — never near-black. For Book-a-Call / dark CTA blocks reusetemplate-parts/section-cta-dark.php. - Closing "Get started" CTA — trust row + Book a Call on the right (HARD). The dark
section-cta-darkthat closes every page (right above the footer) ALWAYS ships:- a green-check trust row under the buttons (
trustprop, 3 short items — reuse claims already on the page verbatim when they fit; otherwise the house default trioFree tier available/Enterprise SLA/No vendor lock-in), and - two buttons — the page's own primary CTA (style
ghost) on the LEFT andBook a Call(stylelight,modal => true) on the RIGHT. Book a Call is always present and always the rightmost button; if the page's original CTA legitimately carries extra buttons, keep them, but Book a Call stays rightmost. Every page with a modal button must also includetemplate-parts/section-book-call-modal(default copy is fine). This rule is the user's standing, explicit approval for adding theBook a Calllabel and the trust row on redesigns — the Copy is untouchable rule still governs every other word (the left button keeps the page's original label/URL verbatim).
- a green-check trust row under the buttons (
Target Accessibility ≥ 95, SEO 100, CLS 0.
- A11y: contrast ≥ 4.5:1 (muted text
--text-captionor darker on white); in-text links underlined; heading order never skips (h2→h3); one<main>; icon-only buttons getaria-label+type="button"; every<img>hasalt. - Performance: right-size images (≤ 2× display width); every
<img>has explicitwidth/height;loading="lazy"for below-the-fold media. - Responsive: no horizontal scroll on mobile; stack 2-col rows ≤ 900px. Don't put
overflowon<html>/<body>(breaksposition: sticky).
A card must never sit alone in a row, and a last row must never have empty slots on the
right. This is one of the most-often-missed rules — treat it as a blocking defect, not
polish. It applies to every grid on the page, including the item grids inside
reusable blocks — section-feature-spotlight items, section-feature-cards,
section-compliance-cards, section-link-cards, section-feature-rows — not just grids you
hand-build.
Why it keeps slipping through: most blocks default their grid to
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)). That looks fine while you author it, but at the 1152px content widthauto-fitsilently lands on 3 columns — so 4 items render 3 + 1, 5 items render 3 + 2 (one short), 7 render 3 + 3 + 1. The block will not fix this for you; you must override the column count per instance to match the item count.
Mandatory post-build check (do this every time): after the page renders, count the cards in every grid (including block item-grids) and confirm the last row is full. If any row is short, apply the matching fix below. Verify it in a real render, not by eyeballing the PHP.
Match the layout to the count:
- Count divides the column count evenly (e.g. 4 in a 2-up, 6 in a 3-up, 8 in a 4-up) → leave it.
- Even but not a clean multiple (e.g. 4 in a 3-up, 8 in a 3-up) → change the
column count so it divides: 4 →
2×2, 8 →4×2. Never ship a 3-up that orphans a card. - Odd with a multi-card orphan row (e.g. 7 in a 4-up, 5 in a 3-up) → keep the base
columns but stretch the last row's cards to span the full width equally, using a
fine-grained track grid:
/* 7 cards, 4-up: 12-track → 4 per row (span 3), last 3 stretch (span 4) */ .grid { grid-template-columns: repeat(12, minmax(0,1fr)); } .grid > .card { grid-column: span 3; } .grid > .card:nth-last-child(-n+3):nth-child(n+5) { grid-column: span 4; }
- Single orphan (e.g. 5 in a 3-up leaves 1) → don't stretch one card to full width (it
reads half-empty). Re-balance instead: 5 → top row 3 + bottom row 2 stretched (6-track:
cards
span 2, last twospan 3), or2 + 3.
Scope it, don't edit the shared partial. Put the override in the page <style>, scoped to
the page's <main> class + the block's grid class (e.g. .lvs .fs-grid, .lvs .shs-lc-grid),
so other pages using the same block are unaffected. Comment it with the card count it's tuned
for — if the count changes, retune. Breakpoint: cover the whole multi-column range, not
just ≥901px: a ≥901px-only override still leaves the ~872–900px band on the block's
auto-fit 3-up (→ orphan again). Force the fixed column count from where that count first
comfortably fits (@media (min-width: 601px) for a 2-up, @media (min-width: 901px) for a
4-up) and let the block's own responsive rules take over below.
/* Live example — page-live-streaming.php: match each block grid to its item count */
@media (min-width: 901px) { .lvs .shs-lc-grid { grid-template-columns: repeat(4, minmax(0,1fr)); } } /* 8 link cards → 4×2 */
@media (min-width: 601px) { .lvs .fs-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } } /* 4 spotlight items → 2×2 */References: page-live-streaming.php (Related Solutions 8→4×2, AI Features 4→2×2) and
page-self-hosted-llm-ai-agent.php (Supported Models, Take Control, Train AI).
Cards sitting side by side in one row must be the same height — never a ragged row where one card is taller because its heading or text is longer. Uneven card heights read as broken.
- CSS Grid does this by default (
align-items: stretch), so plain grids are usually fine — but confirm it in a real render; a strayalign-items: start/flex-startbreaks it. - Flex-wrap rows must set
align-items: stretch(NOTflex-start) so every card in a flex line takes the tallest sibling's height. Put the card's footer action (a "Read more", a link, a button) onmargin-top: autoso it stays bottom-aligned when a shorter card stretches — the extra space falls between the body and the footer, and all footers line up. - This is a block-level quality rule: if a shared partial ships ragged rows, fix it in
the partial (so every page benefits), not with a page override. Example fix:
section-feature-cards.phpexpandable grid wasalign-items: flex-start→ changed tostretchso its cards are equal-height per row everywhere.
The same expectation holds for any side-by-side pair (split cards, two-column feature rows): matched heights, aligned baselines. Verify it in the render, per row, at desktop width.
Every marketing page — a new build or a redesign — ships the reveal-on-scroll animation. Content blocks fade + slide in as they enter the viewport (headers rise, side blocks slide from their side, card grids/lists cascade), exactly like the home page. It's not optional polish; it's part of the house style, so add it as a matter of course — don't wait to be asked.
It's a self-contained <style> + inline <script> per page (kept off main.js), driven by an
IntersectionObserver that tags blocks with data-reveal + a direction and flips them to
.reveal-in. Full copy-paste snippet, how to build the per-page SELECTORS list, and the
non-negotiable gotchas are in references/reveal-on-scroll.md.
The short version of the gotchas (get these wrong and blocks "pop" instead of easing in):
- Animate
translate, nevertransform(it would fight the cards' hovertransform). - Use
[data-reveal][data-reveal]so the revealtransitionout-specifies a card's owntransition(otherwiseopacityisn't animated). overflow-x: clipon the<main>wrapper contains the horizontal slide without a scrollbar and (unlikeoverflow: hidden) keepsposition: stickyworking.- Never tag a
position: stickyelement or the hero; scope selectors to the page's<main>class so header/footer menus are untouched. - Wrap the hidden state in
@media (prefers-reduced-motion: no-preference); JS-off / reduced motion ⇒ everything visible, no animation.
Reference: page-self-hosted-llm-ai-agent.php (and index.php for the home original).
Each is a template-parts/section-*.php partial. Reuse via get_template_part().
Screenshots, full prop tables and copy-paste snippets are in
references/BLOCKS.md — read it before building a section.
Below the section blocks, BLOCKS.md also documents the Core UI primitives — the single canonical CTA buttons, slider/nav buttons, toggle switch, and the
--gradient-brandblue section background. These are locked: reuse them exactly, never restyle or reinvent. Their CSS ships incss/primitives.css(load it aftercss/tokens.css) — import that file and use the classes; do not re-implement the rules.
| Block | Partial | What it is |
|---|---|---|
| Hero | section-hero.php |
Page opener: gradient bg, eyebrow/h1/lead + CTA buttons + trust row, product visual, optional compliance strip. |
| Split card | section-split-card.php |
Brand-gradient card: heading + paragraphs beside an image. reverse flips sides. |
| Blue statement band | section-split-card.php (dark: true) |
Full-bleed brand dark-blue section, heading + paragraphs in white. Edge-to-edge colour. |
| Scroll-telling | section-why.php |
Pinned title + changing image (left), text track that slides per step on scroll (right); mobile accordion. |
| Key features | section-key-features.php |
Auto-cycling accordion (one item open, progress loader) + product image. reverse supported. |
| Feature cards | section-feature-cards.php |
Grid of gradient cards: coloured circle icon + heading + text + optional "Learn more". |
| Link cards | section-link-cards.php |
Grid of cards (icon + heading + text + "Read more →") that fill brand blue on hover. For related links / SDKs / industries. |
| Bento grid | (pattern on page-self-hosted-server.php) |
Asymmetric 2+3 card grid; large gradient/dark cards with peeking screenshots + media tiles. |
| Dark CTA | section-cta-dark.php |
Brand .shs-dark panel (eyebrow/heading/text/buttons). Use for EVERY dark CTA. |
| Pricing cards | section-pricing-cards.php |
3-card pricing, middle highlighted, Monthly/Yearly toggle. |
| Testimonials carousel | section-testimonials-carousel.php |
Auto-advancing 3-up testimonial carousel with prev/next; optional items override when a page must keep its own verbatim quotes. |
| Cards carousel | section-cards-carousel.php |
Draggable horizontal slider of dark cards, active card centred + sharp, sides scaled down + blurred, infinite loop (last peeks left of first); prev/next + swipe. For use-cases / challenge→solution. |
| Case studies | section-case-studies.php |
Case-study carousel: centred active card, blurred side cards, infinite loop, dots + prev/next. |
| Comparison | section-comparison.php |
Capability vs negative (✕) vs highlighted recommended (✓) column; responsive (cards on mobile). |
| Feature spotlight | section-feature-spotlight.php |
Big flagship blue card (chat mockup + chips) + numbered white cards. For a headline capability + supporting features. |
| Deployment stack | section-deployment.php |
Header + platform chips + dashed "your VPC" container laying an architecture diagram out as native cards (stacked layer groups + arrows, tinted core group, data/optional row) + legend. |
| Compliance cards | section-compliance-cards.php |
Grid of white cards (default 4-up): soft-blue icon tile + green "✓ STATUS" tag, heading, text. For trust/compliance strips or "capability + status" grids. |
| Stats band | section-stats.php |
Full-bleed brand-blue gradient band (same as .cc-section) with a header + flat divided stats: icon tile + big number + label. For "by the numbers" strips. |
| Pricing / feature matrix | section-pricing-matrix.php |
Features × plans comparison in a white card: plan columns (one "Most popular" → blue header + highlighted column), rows grouped under mono labels, cells = green check / em-dash / text, optional legend. |
| Vendor comparison | section-vendor-comparison.php |
"Us vs competitors": capability rows across a featured vendor (raised brand-blue card, checks + wins footer) + N competitor columns (red ✕ on weak rows). CSS grid; the featured column floats. |
| Feature rows | section-feature-rows.php |
Vertical stack of row cards: soft-blue icon tile + heading/description + status pills on the right (green "✓ Available" / blue "⚙ Customizable"). |
| Code + config covers | section-code-config.php |
Syntax-highlighted code editor mockup (dots + filename + Copy) on the left + labelled "what config covers" cards (icon + mono chip + text) on the right, with a footnote. |
| Feature list + media | section-feature-list-media.php |
Two-column split: icon + heading + description rows (hairline dividers) on one side, a framed product image (or a dashed "drop an image" placeholder) on the other. |
| Trust band | section-trust-band.php |
Full-bleed dark brand band (.shs-dark): mono label + row of white customer logos + a grid of headline stats (big number + caption). For a "trusted by" strip under a hero. |
| Sticky timeline nav | section-timeline-nav.php |
Sticky horizontal strip of year/step labels below the fixed header: active label enlarges, click smooth-scrolls to the target section, scrollspy + auto-centring. For history / step-by-step pages. |
Other partials exist (section-choose-app, section-use-kit, section-quick-start,
section-book-call-modal, section-pricing, …) — list them with
ls template-parts/ and open the file's top docblock for params.
When you build a section worth reusing:
- Create
template-parts/section-<name>.php. Accept data via$args(wp_parse_argswith sane defaults) — see existing parts for the pattern. - Tokens only, self-contained: emit the block's
<style>once per request using a$GLOBALS['…_assets']flag (so multiple instances on one page don't duplicate CSS), and unique ids for a11y if it has interactive state. - Make it flexible: optional image,
reverse, any number of items where sensible. - Add a screenshot to
references/screenshots/and an entry (with props + usage) toreferences/BLOCKS.md, plus a row in the table above.
Capture screenshots the same way they were made (Playwright + system Chrome, hide
.header, element screenshot by selector) — see references/BLOCKS.md footer.