# 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. **Hero** — `views/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. **Body** — `the_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 `
` 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 `
![]()
` (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 `
![]()
` (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__content` — `position: 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__heading` — `font-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__intro` — `font-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__media` — `position: 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__vector` — `position: 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-hero` → `page.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.