# 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 dark hero with a background image and a decorative vector on the right, a centered narrow intro section underneath, and the standard editor body below it. 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 three-part composition: 1. **Hero** — `views/partials/page-hero.php` (existing), extended with two new ACF fields and two new visual slots. 2. **Intro** — new `views/partials/page-intro.php`, rendered by `page.php` between the hero and `the_content` when the `intro` ACF field is non-empty. 3. **Body** — `the_content`, unchanged. `page.php` becomes the orchestrator: it decides whether to render the intro partial based on the `intro` ACF value, then renders the body. Sidebar behavior (already gated by `hasSidebar()` and the `Page Sidebar` field group) is preserved. `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: - Read the two new fields (`hero_image`, `hero_vector`). - Render `hero_image` as an absolutely-positioned `` inside a new `.page-hero__media` container, masked with a left-to-right fade so the text on the left remains readable. - Render `hero_vector` as an absolutely-positioned, right-anchored, bottom-anchored `` inside a new `.page-hero__vector` container, with `aria-hidden="true"` and `role="presentation"`. - Stop rendering the `intro` paragraph inside the hero. The `intro` field is now rendered in the new page-intro partial. (The heading still renders inside the hero, so the page title and the optional heading override work as before.) - Keep the `heading`, `breadcrumbs`, and `call_to_actions` rendering unchanged. - When no `hero_image` is set, the existing `background_color` style (or the default `bg-dark` token) is the only background. Both new images are `loading="lazy"` and decorative. ## Hero Partial CSS — `views/partials/page-hero.css` (new) The partial needs a small CSS file for the absolute positioning and mask treatment that Tailwind utilities do not express cleanly. A dedicated file follows the project's existing pattern (`contact-block.css`, `homepage-hero.css`). Rules: - `.page-hero` — the existing wrapper. Add `position: relative` and `isolation: isolate` so the new media layers stack predictably. - `.page-hero__media` — `position: absolute; inset: 0; z-index: 0; mask-image: linear-gradient(to right, transparent 0%, black 30%);` (mirrored `-webkit-mask-image`). The image inside it is `width: 100%; height: 100%; object-fit: cover;`. - `.page-hero__vector` — `position: absolute; right: 0; bottom: 0; width: clamp(18rem, 38vw, 32rem); height: auto; z-index: 1; pointer-events: none;`. The mobile breakpoint (≤ 767px) repositions the vector to fill the lower portion of the hero: `width: 100vw; right: auto; left: 0; bottom: 0;` and the `__media` mask switches to a top-to-bottom gradient so the text remains readable. - `.page-hero__content` (or the existing `.content-wrapper` div inside the hero) — `position: relative; z-index: 10;` so text always sits above the media layers. 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. ## Intro Partial — `views/partials/page-intro.php` (new) A small PHP partial that renders a single centered column: - Wrapper: `
` - Container: `
` — note: the `container` Tailwind utility is intentionally **not** used here. The project's `styles/base/global.css` defines `.container { max-width: 80.5rem; }` (1288px), and because `global.css` is loaded after the Tailwind utilities in the cascade, `container` would override `max-w-3xl` (768px) on desktop, producing an over-wide intro. Use only `mx-auto max-w-3xl` so the cap applies. - Inner: a single `

` element with `text-lg lg:text-xl leading-relaxed` containing `wp_kses_post( $intro )`. No new fields, no new CSS file — Tailwind utilities cover the layout. ## page.php Wiring `page.php` changes from rendering the body directly to: 1. Output the article container (existing behavior). 2. If `get_field( 'intro' )` is non-empty, call `get_template_part( 'views/partials/page-intro' )` **as a sibling of** the `entry-content` div (not inside it). The intro must be a peer of the body in the layout flow, not a child, so the test's order assertion `introBox.y < contentBox.y` holds (when intro is a child of entry-content, the two bounding boxes share the same top y and the order check fails). 3. Render `the_content` (existing behavior). 4. Render the sidebar if `hasSidebar()` (existing behavior). The hero is still 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.php` loads the page-intro partial (if intro is set) → `the_content` renders 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 existing dark background color (or the default dark text-only style). - `hero_vector` empty → no vector renders; the right side of the hero is empty space. - `intro` empty → page-intro partial is not loaded; the page goes straight from hero to body. - `heading` empty → falls back to `getTheTitle()` (existing behavior, unchanged). - Background color set without a hero image → existing behavior; no change. - 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 in that order. When hero image is set, it is visible behind the hero. When vector is set, it sits on the right of the hero. - Mobile (402px): same order. Vector fills the lower portion of the hero. Intro is centered and within the 48rem max-width. No clipped text. - 320px and 767px: nothing clips, vector scales proportionally, intro remains readable. - A page with no `intro` field renders hero + body with no empty intro wrapper. - A page with no `hero_image` and no `hero_vector` renders the existing dark 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 page title (or heading override) and, when supplied, a background image and a decorative vector on the right. 3. When the `intro` field is set, a centered narrow column appears under the hero. When empty, it is omitted entirely (no empty wrapper). 4. The body renders `the_content` as before, with the standard block editor. 5. The hero background image is masked so text on the left remains readable at all viewports. 6. The vector scales proportionally from 320px through desktop and is repositioned below 768px to fill the lower portion of the hero. 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.