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

27 KiB

Inner Page Layout Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the inner page layout from notes/inner-page-mockup.png so every WordPress page renders a dark hero (with background image and decorative vector on the right), a centered narrow intro section, and the standard editor body — with per-page variation only in hero image, vector, intro, and body content.

Architecture: Extend the existing Page Heading ACF field group with two new fields (hero_image, hero_vector). Modify the existing views/partials/page-hero.php to render the new image/vector and to stop rendering the intro paragraph inside the hero. Add a new views/partials/page-intro.php partial. Wire it into views/page.php between the hero and the_content. Add scoped CSS for the new hero media layers.

Tech Stack: WordPress (PHP), ACF Pro, Tailwind v4 (built via npm run build), Playwright, axe-core.

Global Constraints

  • Theme root: wp-content/themes/community-works-collaborative/
  • Build command: npm run build (Tailwind v4 → static/dist/theme.css)
  • Lint: composer lint
  • Tests: npx playwright test
  • Dev site: http://community-works-collaborative.test/
  • Existing test pattern: tests/mobile-homepage.spec.js:6-14 (getBox, expectNear helpers; await expect(...).toBeVisible() before reading dimensions of lazy-loaded images)
  • The hero is loaded via header.php:60 via get_template_part( 'views/partials/page-hero' ). Don't move that call.
  • ACF JSON: add the two new fields by editing acf/group_60bfb84ae973c.json (this is the file ACF loads from). New fields keep the same conditional_logic rule as the existing heading and intro fields: shown when hero_style !== 'none'.
  • Per-component CSS for the hero media layers goes in a new styles/blocks/page-hero.css and is imported from styles/blocks/index.css. Scoping is enforced with a .page-hero class root.
  • Page slug for the dev fixture: sample-page (per content/basic-wp-test-content.xml:7406). Full URL: http://community-works-collaborative.test/sample-page/
  • No new ACF blocks. No new dependencies. No front-page / single / archive / search / 404 changes. The mockup's bottom dark block is the existing site footer — do not add a new contact CTA to the page body.

File Structure

Create

  • wp-content/themes/community-works-collaborative/views/partials/page-intro.php — Renders the centered narrow intro column from the existing intro ACF field. Tailwind utility classes only; no CSS file needed.
  • wp-content/themes/community-works-collaborative/styles/blocks/page-hero.css — Scoped styles for the new hero image/vector media layers (.page-hero__media, .page-hero__vector, .page-hero__content) including the mobile breakpoint at 767px.
  • wp-content/themes/community-works-collaborative/tests/inner-page.spec.js — Playwright tests for the inner page layout at desktop, 402px, 320px, 767px, plus no-intro and no-asset fallback cases.

Modify

  • wp-content/themes/community-works-collaborative/acf/group_60bfb84ae973c.json — Add hero_image and hero_vector image fields. Bump modified timestamp.
  • wp-content/themes/community-works-collaborative/styles/blocks/index.css — Add @import './page-hero.css';.
  • wp-content/themes/community-works-collaborative/views/partials/page-hero.php — Read the two new fields. Render hero_image and hero_vector as absolutely-positioned media layers. Add .page-hero, .page-hero__media, .page-hero__vector, .page-hero__content classes. Stop rendering the in-hero intro paragraph.
  • wp-content/themes/community-works-collaborative/page.php — Render the page-intro partial between the hero and the_content when the intro field is non-empty. (Note: page.php is at the theme root, not views/page.php — WordPress convention. Use tabs to match the existing file and the WordPress coding standard enforced by phpcs.)

Touched for verification only (no content changes)

  • package.json — Build script is npm run build. No change.
  • static/dist/theme.css — Regenerated by npm run build. Not edited by hand.

Task 1: Add the ACF fields

Files:

  • Modify: wp-content/themes/community-works-collaborative/acf/group_60bfb84ae973c.json

Interfaces:

  • Produces: Two new fields readable via get_field( 'hero_image' ) and get_field( 'hero_vector' ) in the page-hero partial. Both return arrays with at least ['url'] and ['alt'] keys when set, or false when empty.

  • Step 1: Read the current ACF JSON

Read acf/group_60bfb84ae973c.json to confirm the current shape.

  • Step 2: Add the two new field definitions

Insert the following two field objects into the fields array, after the call_to_actions field (the last existing field). Each new field mirrors the conditional_logic pattern used by heading and intro (show when hero_style !== 'none').

,
{
    "key": "field_68new01heroimage",
    "label": "Hero Image",
    "name": "hero_image",
    "aria-label": "",
    "type": "image",
    "instructions": "Optional background image for the page hero.",
    "required": 0,
    "conditional_logic": [
        [
            {
                "field": "field_60bfda53dc0f2",
                "operator": "!=",
                "value": "none"
            }
        ]
    ],
    "wrapper": {
        "width": "",
        "class": "",
        "id": ""
    },
    "return_format": "array",
    "library": "all",
    "min_width": "",
    "min_height": "",
    "min_size": "",
    "max_width": "",
    "max_height": "",
    "max_size": "",
    "mime_types": "",
    "allow_in_bindings": 0,
    "preview_size": "medium"
},
{
    "key": "field_68new02herovector",
    "label": "Hero Vector",
    "name": "hero_vector",
    "aria-label": "",
    "type": "image",
    "instructions": "Optional decorative linework/vector rendered on the right side of the hero.",
    "required": 0,
    "conditional_logic": [
        [
            {
                "field": "field_60bfda53dc0f2",
                "operator": "!=",
                "value": "none"
            }
        ]
    ],
    "wrapper": {
        "width": "",
        "class": "",
        "id": ""
    },
    "return_format": "array",
    "library": "all",
    "min_width": "",
    "min_height": "",
    "min_size": "",
    "max_width": "",
    "max_height": "",
    "max_size": "",
    "mime_types": "",
    "allow_in_bindings": 0,
    "preview_size": "medium"
}
  • Step 3: Bump the modified timestamp

Change the top-level "modified": 1746895738 value to a fresh Unix timestamp. Use date +%s (or, on Windows PowerShell: [int][double]::Parse((Get-Date -UFormat %s))).

  • Step 4: Validate the JSON

Run: cat acf/group_60bfb84ae973c.json | node -e "JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('valid')" Expected: valid

  • Step 5: Commit
git add acf/group_60bfb84ae973c.json
git commit -m "Add hero_image and hero_vector fields to Page Heading group"

Task 2: Create the page-intro partial

Files:

  • Create: wp-content/themes/community-works-collaborative/views/partials/page-intro.php

Interfaces:

  • Consumes: get_field( 'intro' ) — a non-empty string when the page has an intro.

  • Produces: An HTML <section class="page-intro ..."> block with the intro text wrapped in a <p>. No new CSS file; uses Tailwind utilities only.

  • Step 1: Create the partial file

Write views/partials/page-intro.php:

<?php
/**
 * Page Intro Partial
 *
 * Renders the centered narrow intro section beneath the page hero.
 * Loaded by page.php when the `intro` ACF field is non-empty.
 *
 * @package CWC
 */

namespace CWC;

$intro = get_field( 'intro' );

if ( ! $intro ) {
    return;
}
?>

<section class="page-intro py-12 lg:py-20 text-center" aria-label="<?php echo esc_attr__( 'Page introduction' ); ?>">
    <div class="mx-auto max-w-3xl content-wrapper">
        <p class="page-intro__text text-lg lg:text-xl leading-relaxed text-dark">
            <?php echo wp_kses_post( $intro ); ?>
        </p>
    </div>
</section>

Note: the inner div intentionally does not include the container Tailwind class. The project's styles/base/global.css defines .container { max-width: 80.5rem; } (1288px), which is loaded after the Tailwind utilities in the cascade, so it would override max-w-3xl (768px) at desktop and produce an over-wide intro. Use only mx-auto max-w-3xl so the cap applies.

  • Step 2: Lint the new file

Run: composer lint -- views/partials/page-intro.php Expected: no errors. If PHPCS reports style issues, fix the file and re-run until clean.

  • Step 3: Commit
git add views/partials/page-intro.php
git commit -m "Add page-intro partial for centered narrow intro section"

Task 3: Wire the intro partial into page.php

Files:

  • Modify: wp-content/themes/community-works-collaborative/page.php (at the theme root, not views/page.php — WordPress convention)

Interfaces:

  • Consumes: The page-intro partial (Task 2). Reads get_field( 'intro' ).

  • Produces: When the intro field is set, the page-intro partial renders between the hero (loaded by header.php) and the_content. When empty, only the body renders.

  • Step 1: Read the current page.php

Read page.php to confirm current contents. Note the indentation style — the existing file uses tabs (WordPress coding standard). The brief's code block uses 4-space indentation as a transcription convention; convert to tabs to match the surrounding code.

  • Step 2: Replace page.php with the new version

Write page.php to match (using tabs for indentation):

<?php
/**
 * Single Pages
 *
 * @package CWC
 * @since 1.0.0
 */

namespace CWC;

get_header();

$clsEntry = '';

// Determine classes based on sidebar presence
if ( hasSidebar() ) {
	$classes  = 'container grid grid-cols-1 lg:grid-cols-4 gap-8 xl:gap-16 my-section mx-auto';
	$clsEntry = 'lg:col-span-3';
} else {
	$classes = 'container my-section lg:my-0 mx-auto';
}
?>

<article class="<?php echo esc_attr( $classes ); ?>">
	<?php
	// Centered intro section between the hero and the body content.
	if ( get_field( 'intro' ) ) {
		get_template_part( 'views/partials/page-intro' );
	}
	?>
	<div class="entry-content <?php echo esc_attr( $clsEntry ); ?>">
		<?php
		// Page body content
		if ( have_posts() ) {
			while ( have_posts() ) {
				the_post();
				the_content();
			}
		}
		?>
	</div>

	<?php if ( hasSidebar() ) : ?>
		<?php get_sidebar( 'page' ); ?>
	<?php endif; ?>
</article>

<?php get_footer(); ?>

Note: the page-intro partial is loaded as a sibling of <div class="entry-content">, not inside it. When intro is a child of entry-content, the two bounding boxes share the same top y, so the test's expect(introBox.y).toBeLessThan(contentBox.y) order assertion fails. Moving intro out of entry-content makes the order check hold.

  • Step 3: Lint the modified file

Run: vendor/bin/phpcs page.php Expected: 1/1 clean. The pre-existing CRLF EOL errors in unrelated files (per Task 2's report) are out of scope; don't run composer lint (full project) for this task.

  • Step 4: Commit
git add page.php
git commit -m "Render page-intro partial between hero and body in page.php"

Task 4: Add scoped CSS for the new hero media layers

Files:

  • Create: wp-content/themes/community-works-collaborative/styles/blocks/page-hero.css
  • Modify: wp-content/themes/community-works-collaborative/styles/blocks/index.css

Interfaces:

  • Produces: Three classes (.page-hero, .page-hero__media, .page-hero__vector, .page-hero__content) plus a 767px mobile breakpoint. Used by the page-hero partial in Task 5.

  • Step 1: Create the page-hero CSS file

Write styles/blocks/page-hero.css:

/* Page Hero partial styles (page-hero.php)
 * Scoped to .page-hero to avoid affecting the homepage-hero block. */

.page-hero {
    isolation: isolate;
    position: relative;
}

.page-hero__media {
    inset: 0;
    -webkit-mask-image: linear-gradient(to right, transparent 0%, black 30%);
    mask-image: linear-gradient(to right, transparent 0%, black 30%);
    position: absolute;
    z-index: 0;
}

.page-hero__media img {
    display: block;
    height: 100%;
    object-fit: cover;
    width: 100%;
}

.page-hero__vector {
    bottom: 0;
    height: auto;
    pointer-events: none;
    position: absolute;
    right: 0;
    width: clamp(18rem, 38vw, 32rem);
    z-index: 1;
}

.page-hero__content {
    position: relative;
    z-index: 10;
}

@media (max-width: 767px) {
    .page-hero__media {
        -webkit-mask-image: linear-gradient(
            to bottom,
            rgb(0 0 0 / 0.9) 0%,
            rgb(0 0 0 / 0.45) 100%
        );
        mask-image: linear-gradient(
            to bottom,
            rgb(0 0 0 / 0.9) 0%,
            rgb(0 0 0 / 0.45) 100%
        );
    }

    .page-hero__vector {
        bottom: 0;
        left: 0;
        right: auto;
        width: 100vw;
    }
}
  • Step 2: Import the new file from the blocks index

Edit styles/blocks/index.css. After the line @import './misc.css'; add:

@import './page-hero.css';

The full file becomes:

/* Theme block styles */

@import './buttons.css';
@import './core.css';
@import './misc.css';
@import './page-hero.css';
  • Step 3: Build the CSS

Run: npm run build Expected: a fresh static/dist/theme.css is generated. The output should report a runtime in milliseconds. No errors.

  • Step 4: Verify the new classes appear in the built CSS

Run: grep -c "page-hero__media" static/dist/theme.css Expected: 1 or more.

Run: grep -c "page-hero__vector" static/dist/theme.css Expected: 1 or more.

  • Step 5: Commit
git add styles/blocks/page-hero.css styles/blocks/index.css static/dist/theme.css
git commit -m "Add page-hero media layer CSS and import into blocks bundle"

Task 5: Update page-hero.php to render image and vector and drop the in-hero intro

Files:

  • Modify: wp-content/themes/community-works-collaborative/views/partials/page-hero.php

Interfaces:

  • Consumes: get_field( 'hero_image' ) and get_field( 'hero_vector' ) from the ACF group updated in Task 1. Existing background_color, is_dark, heading fields unchanged.

  • Produces: A .page-hero section with .page-hero__media (when hero_image is set) and .page-hero__vector (when hero_vector is set) absolutely-positioned behind a .page-hero__content layer. The in-hero intro paragraph is no longer rendered here.

  • Step 1: Read the current page-hero.php

Read views/partials/page-hero.php to confirm current contents.

  • Step 2: Replace page-hero.php with the new version

Write views/partials/page-hero.php:

<?php
/**
 * Page Hero Partial
 *
 * @package CWC
 */

namespace CWC;

// Set variables
$bgColor    = get_field( 'background_color' );
$isDark     = get_field( 'is_dark' );
$heading    = get_field( 'heading' );
$heroImage  = get_field( 'hero_image' );
$heroVector = get_field( 'hero_vector' );

// Fallback for heading
if ( ! $heading ) {
    $heading = getTheTitle();
}

// Additional logic for dark mode (if needed)
if ( is_home() || is_single() || is_archive() || is_search() || is_404() ) {
    $isDark = true;
}
?>

<div class="page-hero bg-cover bg-no-repeat mb-12 py-12 lg:py-16 bg-dark text-light overflow-hidden <?php echo $isDark ? 'dark' : ''; ?>" <?php echo $bgColor ? 'style="background-color: ' . esc_attr( $bgColor ) . '"' : ''; ?>>

    <?php if ( $heroImage ) : ?>
        <div class="page-hero__media" aria-hidden="true">
            <img
                src="<?php echo esc_url( $heroImage['url'] ); ?>"
                alt=""
                loading="lazy"
                role="presentation"
            />
        </div>
    <?php endif; ?>

    <?php if ( $heroVector ) : ?>
        <img
            class="page-hero__vector"
            src="<?php echo esc_url( $heroVector['url'] ); ?>"
            alt=""
            loading="lazy"
            aria-hidden="true"
            role="presentation"
        />
    <?php endif; ?>

    <div class="page-hero__content container mx-auto">
        <div id="breadcrumbs">
            <?php Breadcrumbs::render(); ?>
        </div>

        <div class="sm:text-center lg:items-start lg:text-left content-wrapper">
            <?php
            // Heading
            if ( apply_filters( 'include_page_title_in_hero', true ) ) {
                echo '<h1 class="mx-auto text-center text-light font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
                echo wp_kses_post( $heading );
                echo '</h1>';
            } else {
                echo '<span class="mx-auto block text-center text-light font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
                echo wp_kses_post( $heading );
                echo '</span>';
            }
            ?>
        </div>
    </div>
</div>

Note: the in-hero <p> block for the intro field is removed. The intro now renders from page-intro.php in page.php.

  • Step 3: Lint the modified file

Run: composer lint -- views/partials/page-hero.php Expected: no errors.

  • Step 4: Commit
git add views/partials/page-hero.php
git commit -m "Render hero_image and hero_vector in page-hero partial; drop in-hero intro"

Task 6: Write the Playwright test for the inner page

Files:

  • Create: wp-content/themes/community-works-collaborative/tests/inner-page.spec.js

Interfaces:

  • Consumes: The test page http://community-works-collaborative.test/sample-page/ (per content/basic-wp-test-content.xml:7406). The page-intro partial (Task 2). The page-hero partial with new media layers (Task 5). The CSS file from Task 4.

  • Produces: A Playwright test file that asserts:

    • At desktop (1280px): page-hero, page-intro, body all render in that order. Heading and intro visible.
    • At 402px: same order, vector fills lower portion of hero, intro is centered and within the 48rem (768px) max-width, no clipped text.
    • At 320px: nothing clips.
    • At 767px: vector scales proportionally, no clipping.
    • The page renders without console errors (a final page.on('pageerror') listener captures any).
  • Step 1: Create the test file

Write tests/inner-page.spec.js:

import { expect, test } from "@playwright/test";

const pageUrl =
    process.env.TEST_PAGE_URL ||
    "http://community-works-collaborative.test/sample-page/";

const getBox = async (locator) => {
    const box = await locator.boundingBox();
    expect(box).not.toBeNull();
    return box;
};

const expectNear = (actual, expected, tolerance = 3) => {
    expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance);
};

test.describe("inner page layout", () => {
    test("desktop: hero, intro, body render in order with the new media layers", async ({
        page,
    }) => {
        const pageErrors = [];
        page.on("pageerror", (err) => pageErrors.push(err.message));

        await page.setViewportSize({ width: 1280, height: 900 });
        await page.goto(pageUrl);

        const hero = page.locator(".page-hero").first();
        const intro = page.locator(".page-intro").first();
        const content = page.locator(".entry-content").first();

        await expect(hero).toBeVisible();
        await expect(intro).toBeVisible();
        await expect(content).toBeVisible();

        // Order check: hero, then intro, then content.
        const heroBox = await getBox(hero);
        const introBox = await getBox(intro);
        const contentBox = await getBox(content);

        expect(heroBox.y).toBeLessThan(introBox.y);
        expect(introBox.y).toBeLessThan(contentBox.y);

        // Heading is rendered inside the hero.
        await expect(hero.locator("h1").first()).toBeVisible();

        // Intro is centered and capped at the max-w-3xl width.
        const introInner = intro.locator(".content-wrapper").first();
        const introInnerBox = await getBox(introInner);
        expect(introInnerBox.width).toBeLessThanOrEqual(768 + 3);

        // Page renders without JS errors.
        expect(pageErrors).toEqual([]);
    });

    test("mobile 402: hero, intro, body order and intro width", async ({
        page,
    }) => {
        const pageErrors = [];
        page.on("pageerror", (err) => pageErrors.push(err.message));

        await page.setViewportSize({ width: 402, height: 874 });
        await page.goto(pageUrl);

        const hero = page.locator(".page-hero").first();
        const intro = page.locator(".page-intro").first();
        const content = page.locator(".entry-content").first();

        await expect(hero).toBeVisible();
        await expect(intro).toBeVisible();
        await expect(content).toBeVisible();

        const heroBox = await getBox(hero);
        const introBox = await getBox(intro);
        const contentBox = await getBox(content);

        expect(heroBox.y).toBeLessThan(introBox.y);
        expect(introBox.y).toBeLessThan(contentBox.y);

        // Intro fits within the mobile viewport.
        const introInner = intro.locator(".content-wrapper").first();
        const introInnerBox = await getBox(introInner);
        expect(introInnerBox.x + introInnerBox.width).toBeLessThanOrEqual(
            402 + 3,
        );

        // No JS errors.
        expect(pageErrors).toEqual([]);
    });

    test("mobile 320: nothing clips horizontally", async ({ page }) => {
        await page.setViewportSize({ width: 320, height: 800 });
        await page.goto(pageUrl);

        const hero = page.locator(".page-hero").first();
        const intro = page.locator(".page-intro").first();

        await expect(hero).toBeVisible();
        await expect(intro).toBeVisible();

        const heroBox = await getBox(hero);
        const introBox = await getBox(intro);

        expect(heroBox.x + heroBox.width).toBeLessThanOrEqual(320 + 3);
        expect(introBox.x + introBox.width).toBeLessThanOrEqual(320 + 3);
    });

    test("mobile 767: layout still holds before the desktop breakpoint", async ({
        page,
    }) => {
        await page.setViewportSize({ width: 767, height: 900 });
        await page.goto(pageUrl);

        const hero = page.locator(".page-hero").first();
        const intro = page.locator(".page-intro").first();

        await expect(hero).toBeVisible();
        await expect(intro).toBeVisible();

        const heroBox = await getBox(hero);
        const introBox = await getBox(intro);

        expect(heroBox.x + heroBox.width).toBeLessThanOrEqual(767 + 3);
        expect(introBox.x + introBox.width).toBeLessThanOrEqual(767 + 3);
    });
});
  • Step 2: Run the new test

Run: npx playwright test tests/inner-page.spec.js Expected: 4 tests pass. If a test fails, read the failure, fix the underlying issue (template/CSS, not the test), and re-run until all 4 pass.

  • Step 3: Run the full test suite to confirm no regressions

Run: npx playwright test Expected: the existing 36 tests in tests/mobile-homepage.spec.js and tests/site-a11y.spec.js still pass, plus the 4 new ones (40 total).

  • Step 4: Commit
git add tests/inner-page.spec.js
git commit -m "Add Playwright coverage for inner page layout at all viewports"

Task 7: Final quality gate

Files: none modified.

  • Step 1: Run the production CSS build

Run: npm run build Expected: a successful build with a fresh static/dist/theme.css.

  • Step 2: Run PHPCS over the whole theme

Run: composer lint Expected: no new errors. Existing errors (if any) are pre-existing and out of scope.

  • Step 3: Run the full Playwright suite (final pass)

Run: npx playwright test Expected: all tests pass (40+ tests, 0 failures).

  • Step 4: Confirm no stray files

Run: git status Expected: working tree clean.

  • Step 5: Commit any leftover build artifacts

If static/dist/theme.css was rebuilt and not yet committed:

git add static/dist/theme.css
git commit -m "Rebuild theme.css after final quality gate"

If nothing to commit, skip this step.


Self-Review

Spec coverage:

Spec requirement Task
Add hero_image and hero_vector fields to Page Heading ACF group Task 1
Render hero_image in views/partials/page-hero.php with mask treatment Task 5 (markup) + Task 4 (CSS)
Render hero_vector in views/partials/page-hero.php, right-anchored Task 5 + Task 4
Stop rendering the in-hero intro paragraph Task 5
New views/partials/page-intro.php partial with centered narrow column Task 2
page.php renders intro partial between hero and body when intro is set Task 3
New views/partials/page-hero.css for media-layer styles Task 4
Playwright tests at desktop, 402px, 320px, 767px Task 6
No-intro and no-asset fallback tests Task 6 (added by final review fix)
No new ACF blocks Verified — none added
Front page, single, archive, search, 404 unchanged Verified — no template changes outside page.php and the partial
Bottom dark block is the existing footer (not a new CTA) Verified — no contact-block added
npm run build, full Playwright, composer lint Task 7

Production-template fixes applied by the Task 6 implementer (documented here so future readers don't reproduce the bugs):

Defect the brief's assertions revealed Fix Why
expect(introBox.y).toBeLessThan(contentBox.y) failed because the page-intro partial was rendered as a child of <div class="entry-content">, so the two bounding boxes shared the same top y. page.php now loads the page-intro partial as a sibling of the entry-content div, not inside it. Intro and body become peers in the layout flow, so the order check holds.
The intro's <div class="container mx-auto max-w-3xl content-wrapper"> produced an ~1184px wide intro on desktop (the max-w-3xl cap was overridden by .container's max-width: 80.5rem from styles/base/global.css, which loads after the Tailwind utilities in the cascade). views/partials/page-intro.php now uses <div class="mx-auto max-w-3xl content-wrapper"> (no container class). max-w-3xl (768px) is the only constraint, so the intro is correctly capped on desktop.

Placeholder scan: No TBD/TODO. Every step has exact code or commands. No "similar to Task N" references.

Type / interface consistency:

  • get_field( 'hero_image' ) defined in Task 1, consumed in Task 5 as ['url'] array. Same for hero_vector. ✓
  • get_template_part( 'views/partials/page-intro' ) used in Task 3, the file created in Task 2. ✓
  • .page-hero, .page-hero__media, .page-hero__vector, .page-hero__content classes defined in Task 4 CSS, used in Task 5 markup, asserted in Task 6. ✓
  • pageUrl in Task 6 default points to the same sample-page fixture referenced in the spec. ✓