Files
CWC/docs/superpowers/specs/2026-07-01-inner-page-layout-design.md

11 KiB

Inner Page Layout

Goal

Build the inner page layout shown in notes/inner-page-mockup.png so that every WordPress page renders the same structure: a blue-to-teal gradient hero with the page title, an optional hero image at its natural size on the right with the bottom hanging over the hero's bottom edge, a decorative vector along the lower-left with breathing room from the bottom, an optional intro text block (the editor-supplied intro field) on the left below the H1, and the standard editor body below the hero. Different pages vary only by hero image, vector, intro text, and body content.

Scope

  • Applies to all page post-type views rendered by page.php (and through it, the page-hero partial that header.php already loads).
  • Does not change the front page, single posts, archives, search, or 404 templates.
  • Does not change the homepage-hero block.
  • Does not add a new contact block to the page body — the dark navy block with the orange rule in the mockup is the existing site footer, not a new CTA.

Architecture

The layout is a fixed two-part composition:

  1. Heroviews/partials/page-hero.php (existing), extended with the blue-to-teal gradient backdrop, two new ACF fields, the hero image rendered on the right (faded to transparent on the left so the heading has a clear backdrop), a decorative vector along the bottom, the heading, and the intro block.
  2. Bodythe_content, unchanged.

page-hero.php is the orchestrator: it renders the gradient backdrop, the optional hero image and vector, the heading, and the intro block all inside a single .page-hero wrapper. The intro ACF field, when set, renders as a two-column heading + body block inside the hero (not as a separate page section). The intro heading is a hard-coded "Our Work" label; the body is the editor-supplied intro text.

page.php continues to render the_content after the hero. Sidebar behavior (already gated by hasSidebar() and the Page Sidebar field group) is preserved. page.php no longer loads the page-intro partial directly — the partial is loaded by page-hero.php instead.

hasPageHeader() in lib/extras.php continues to gate the hero. The function's current behavior (hero shows when hero_style !== 'none') is kept; no change to that filter is required.

ACF Field Group Changes

Add two fields to the existing Page Heading group (acf/group_60bfb84ae973c.json):

  • hero_image — type: image, return_format: array, library: all, preview_size: medium. Conditional: shown when hero_style !== 'none'. Optional.
  • hero_vector — type: image, return_format: array, library: all, preview_size: medium. Conditional: shown when hero_style !== 'none'. Optional.

No other ACF group changes. The existing heading, intro, and call_to_actions fields are reused as-is.

Hero Partial — views/partials/page-hero.php

Changes:

  • The wrapper <div> no longer carries the bg-dark or bg-cover bg-no-repeat Tailwind utilities. The wrapper inherits a blue-to-teal gradient from .page-hero (defined in styles/blocks/page-hero.css), the same gradient used by the homepage-hero block. When the editor sets a background_color ACF field, the inline style attribute on the wrapper overrides the gradient with that solid color.
  • Read the two new fields (hero_image, hero_vector) and the existing intro field.
  • Render the breadcrumbs, the H1, and the intro inside a single .page-hero__content container, before the image and vector markup. The H1 has a dedicated .page-hero__heading class for typography control. The intro, when non-empty, is a single .page-hero__intro div containing only the editor-supplied intro text (no hard-coded heading).
  • Render hero_image as a foreground absolutely-positioned <img> (class .page-hero__media), anchored to the right of the hero with bottom: -10% so the image's bottom hangs over the bottom edge of the hero. The image is shown at its natural size (no object-fit: cover, no mask); it sits above the gradient backdrop and below the content layer.
  • Render hero_vector as an absolutely-positioned <img> (class .page-hero__vector), anchored to the bottom-left of the hero with bottom: clamp(1.5rem, 4vw, 3rem) for spacing from the bottom edge. The vector sits above the image and below the content.
  • When no hero_image is set, the gradient (or the override background_color) is the only background. When no hero_vector is set, the bottom of the hero is just the gradient.

Both new images are loading="lazy" and decorative. No hard-coded copy is rendered.

Hero Partial CSS — styles/blocks/page-hero.css (new)

A dedicated file follows the project's existing pattern (contact-block.css, homepage-hero.css).

Rules:

  • .page-hero — the wrapper. position: relative; isolation: isolate; overflow: hidden; min-height: clamp(31rem, 38vw, 36rem);. background: linear-gradient(83.68deg, #032F46 3.13%, #006196 45.91%, #8FC9E6 96.27%); — same gradient as the homepage-hero block.
  • .page-hero__contentposition: relative; z-index: 10; padding-block: clamp(2rem, 6vw, 4rem); so the heading, intro, and breadcrumbs always sit above the image and vector layers.
  • .page-hero__headingfont-family: var(--font-quincy, 'Quincy', serif); font-weight: 400; font-size: clamp(2.5rem, 6vw, 4.5rem); line-height: 1.05; max-width: 36rem; so the H1 doesn't span the full hero width and stays aligned with the intro text.
  • .page-hero__introfont-size: clamp(1rem, 1.5vw, 1.25rem); line-height: 1.5; max-width: 36rem; opacity: 0.95; so the intro text sits alongside the H1.
  • .page-hero__mediaposition: absolute; right: clamp(1.5rem, 4vw, 4rem); bottom: -10%; width: clamp(18rem, 32vw, 28rem); height: auto; pointer-events: none; z-index: 1;. The image is shown at its natural size.
  • .page-hero__vectorposition: absolute; left: 0; bottom: clamp(1.5rem, 4vw, 3rem); width: clamp(28rem, 60vw, 56rem); height: auto; pointer-events: none; z-index: 5;. The vector is sized to span the lower-left of the hero with breathing room from the bottom edge.
  • Mobile (≤ 767px) breakpoint: hero min-height becomes clamp(28rem, 130vw, 40rem); image shrinks to 60vw and sits flush right (right: 0); vector shrinks to 80vw.

These rules are scoped to .page-hero and only affect the page-hero partial. They do not affect the homepage-hero block, which uses a different root class.

page.php Wiring

page.php continues to render the article container, the_content, and the sidebar (when hasSidebar()). The hero is rendered by header.php before this template runs. No other behavior changes.

Data Flow

  • Editor experience: on every page (and post) edit screen, the existing Page Heading meta box shows the existing fields plus two new ones (Hero Image, Vector). The editor uploads the assets, sets a background color if desired, writes the optional heading override, intro, and CTAs, then writes the body in the standard block editor.
  • Render: header.php loads the page-hero partial → page-hero.php renders the gradient + optional hero image + vector + heading + (when intro is set) the intro block, all inside .page-heropage.php renders the_content for the body blocks.
  • Per-page variation: hero image, vector, intro text, and body content. Everything else is shared.

Error Handling and Edge Cases

  • hero_image empty → hero uses the gradient (or the override background_color) as the only background.
  • hero_vector empty → no vector renders; the bottom of the hero is just the gradient.
  • intro empty → the .page-hero__intro div is not rendered; the hero shows the H1 only.
  • heading empty → falls back to getTheTitle() (existing behavior, unchanged).
  • Background color set without a hero image → existing behavior; the inline style overrides the gradient with the solid color.
  • No sidebar field set → existing hasSidebar() filter returns false → no sidebar (existing behavior).
  • Lazy-loaded images must await load/complete before Playwright reads dimensions (existing project convention; see tests/mobile-homepage.spec.js:641).

Out of Scope

  • New ACF blocks. This layout uses the existing Page Heading meta box, not the block editor.
  • Front page, single posts, archives, search, 404 changes.
  • Homepage-hero block changes.
  • The contact-block, pull-quote, our-work, or other existing blocks.
  • A new contact CTA block in the page body. The mockup's bottom dark block is the existing site footer.
  • Migration of existing pages' content.

Verification

  • Add tests/inner-page.spec.js covering:
    • Desktop (1280px): hero, intro, body render with the intro block as a descendant of the hero. The H1 is visible. The hero exposes the gradient backdrop.
    • Mobile (402px): intro is inside the hero and fits within the viewport. The vector is visible at the bottom of the hero. No clipped text.
    • 320px and 767px: nothing clips, hero and intro remain inside the viewport.
    • A page with no intro field renders the hero without a .page-hero__intro wrapper.
    • A page with no hero_image and no hero_vector renders the existing gradient text-only hero unchanged.
  • Add a test page fixture (or use an existing page) to drive the test. If no fixture exists, the test seeds a page in a beforeAll and tears it down in afterAll.
  • Run npm run build to regenerate static/dist/.
  • Run the full Playwright suite (npx playwright test) — including the existing tests/mobile-homepage.spec.js and tests/site-a11y.spec.js — to confirm no regressions.
  • Run composer lint to catch PHPCS issues.
  • Capture a 1280px and a 402px full-page screenshot of the test fixture for visual comparison against notes/inner-page-mockup.png.

Acceptance Criteria

  1. Every WordPress page renders the new layout when the page-hero partial is active.
  2. The hero shows the blue-to-teal gradient backdrop, the page title (or heading override) in the Quincy serif face, and — when supplied — a hero image at its natural size on the right with the bottom hanging over the hero's bottom edge, and a decorative vector along the lower-left with some spacing from the bottom edge.
  3. When the intro field is set, a single text block appears inside the hero below the H1, in the same left column. When empty, the intro block is omitted entirely.
  4. The body renders the_content as before, with the standard block editor.
  5. No hard-coded copy is rendered by the partial — only the page title, the editor-supplied intro text, and the editor-supplied images.
  6. The hero image and vector render at all viewports without clipping.
  7. The page renders without console errors, axe violations, or PHPCS errors.
  8. Existing single posts, archives, search, and 404 pages render the page-hero partial unchanged.
  9. Existing tests (mobile-homepage, site-a11y) continue to pass.