Refine hero: gradient backdrop, intro inside hero, image on right, vector visible

- styles/blocks/page-hero.css: add blue-to-teal gradient to .page-hero (matches
  homepage-hero). Fix image mask direction (right-to-left fade) so the image
  is visible on the right and fades to transparent on the left. Reposition
  the vector to span the full bottom of the hero at z-index 2 (above the
  media layer, below content). Add .page-hero__intro and .page-hero__intro-inner
  grid rules for the new two-column heading + body block.
- views/partials/page-hero.php: drop bg-dark / bg-cover / bg-no-repeat
  utilities (the gradient comes from CSS now). Simplify the heading render.
  Load the page-intro partial inside .page-hero__content, below the heading.
- views/partials/page-intro.php: rewrite as a fragment for embedding inside
  the hero - a two-column heading (hard-coded 'Our Work') + body grid.
  Returns early when the intro field is empty.
- page.php: remove the page-intro partial call (it's now loaded from
  page-hero.php).
- tests/inner-page.spec.js: switch the selector from .page-intro to
  .page-hero__intro. Update the order assertions to verify the intro is
  a descendant of the hero (not a sibling below it).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Keith Solomon
2026-07-01 15:25:39 -05:00
co-authored by Claude
parent 342ae015c7
commit e221f62ef7
6 changed files with 2383 additions and 3369 deletions
-6
View File
@@ -22,12 +22,6 @@ if ( hasSidebar() ) {
?> ?>
<article class="<?php echo esc_attr( $classes ); ?>"> <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 ); ?>"> <div class="entry-content <?php echo esc_attr( $clsEntry ); ?>">
<?php <?php
// Page body content // Page body content
+2274 -3286
View File
File diff suppressed because it is too large Load Diff
+26 -6
View File
@@ -2,33 +2,37 @@
* Scoped to .page-hero to avoid affecting the homepage-hero block. */ * Scoped to .page-hero to avoid affecting the homepage-hero block. */
.page-hero { .page-hero {
background:
linear-gradient(83.68deg, #032F46 3.13%, #006196 45.91%, #8FC9E6 96.27%);
isolation: isolate; isolation: isolate;
overflow: hidden;
position: relative; position: relative;
} }
.page-hero__media { .page-hero__media {
inset: 0; inset: 0;
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 30%); -webkit-mask-image: linear-gradient(to left, transparent 0%, black 50%);
mask-image: linear-gradient(to right, transparent 0%, black 30%); mask-image: linear-gradient(to left, transparent 0%, black 50%);
position: absolute; position: absolute;
z-index: 0; z-index: 1;
} }
.page-hero__media img { .page-hero__media img {
display: block; display: block;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
object-position: right center;
width: 100%; width: 100%;
} }
.page-hero__vector { .page-hero__vector {
bottom: 0; bottom: 0;
height: auto; height: auto;
left: 0;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
right: 0; width: 100%;
width: clamp(18rem, 38vw, 32rem); z-index: 2;
z-index: 1;
} }
.page-hero__content { .page-hero__content {
@@ -36,6 +40,22 @@
z-index: 10; z-index: 10;
} }
.page-hero__intro {
margin-top: clamp(2rem, 6vw, 4rem);
}
.page-hero__intro-inner {
display: grid;
gap: clamp(1.5rem, 4vw, 3rem);
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.page-hero__intro-inner {
grid-template-columns: 1fr 2fr;
}
}
@media (max-width: 767px) { @media (max-width: 767px) {
.page-hero__media { .page-hero__media {
-webkit-mask-image: linear-gradient( -webkit-mask-image: linear-gradient(
+32 -23
View File
@@ -26,7 +26,7 @@ const expectNear = (actual, expected, tolerance = 3) => {
}; };
test.describe("inner page layout", () => { test.describe("inner page layout", () => {
test("desktop: hero, intro, body render in order with the new media layers", async ({ test("desktop: hero, intro, body render with intro inside hero", async ({
page, page,
}) => { }) => {
const pageErrors = []; const pageErrors = [];
@@ -36,28 +36,34 @@ test.describe("inner page layout", () => {
await page.goto(pageUrl); await page.goto(pageUrl);
const hero = page.locator(".page-hero").first(); const hero = page.locator(".page-hero").first();
const intro = page.locator(".page-intro").first(); const intro = page.locator(".page-hero__intro").first();
const content = page.locator(".entry-content").first(); const content = page.locator(".entry-content").first();
await expect(hero).toBeVisible(); await expect(hero).toBeVisible();
await expect(intro).toBeVisible(); await expect(intro).toBeVisible();
await expect(content).toBeVisible(); await expect(content).toBeVisible();
// Order check: hero, then intro, then content. // Intro lives INSIDE the hero.
const heroBox = await getBox(hero); const heroBox = await getBox(hero);
const introBox = await getBox(intro); const introBox = await getBox(intro);
const contentBox = await getBox(content); const contentBox = await getBox(content);
expect(heroBox.y).toBeLessThan(introBox.y); // Intro is a descendant of the hero (same x, y is between hero top and bottom).
expect(introBox.y).toBeLessThan(contentBox.y); expect(introBox.x).toBeGreaterThanOrEqual(heroBox.x - 1);
expect(introBox.y).toBeGreaterThanOrEqual(heroBox.y - 1);
expect(introBox.y + introBox.height).toBeLessThanOrEqual(
heroBox.y + heroBox.height + 1,
);
// Body renders below the hero.
expect(contentBox.y).toBeGreaterThanOrEqual(heroBox.y + heroBox.height - 1);
// Heading is rendered inside the hero. // Heading is rendered inside the hero.
await expect(hero.locator("h1").first()).toBeVisible(); await expect(hero.locator("h1").first()).toBeVisible();
// Intro is centered and capped at the max-w-3xl width. // Intro contains a heading and a body paragraph.
const introInner = intro.locator(".content-wrapper").first(); await expect(intro.locator(".page-hero__intro-heading").first()).toBeVisible();
const introInnerBox = await getBox(introInner); await expect(intro.locator(".page-hero__intro-text").first()).toBeVisible();
expect(introInnerBox.width).toBeLessThanOrEqual(768 + 3);
// Page renders without JS errors. // Page renders without JS errors.
expect(pageErrors).toEqual([]); expect(pageErrors).toEqual([]);
@@ -73,7 +79,7 @@ test.describe("inner page layout", () => {
await page.goto(pageUrl); await page.goto(pageUrl);
const hero = page.locator(".page-hero").first(); const hero = page.locator(".page-hero").first();
const intro = page.locator(".page-intro").first(); const intro = page.locator(".page-hero__intro").first();
const content = page.locator(".entry-content").first(); const content = page.locator(".entry-content").first();
await expect(hero).toBeVisible(); await expect(hero).toBeVisible();
@@ -84,15 +90,18 @@ test.describe("inner page layout", () => {
const introBox = await getBox(intro); const introBox = await getBox(intro);
const contentBox = await getBox(content); const contentBox = await getBox(content);
expect(heroBox.y).toBeLessThan(introBox.y); // Intro inside the hero.
expect(introBox.y).toBeLessThan(contentBox.y); expect(introBox.x).toBeGreaterThanOrEqual(heroBox.x - 1);
expect(introBox.y).toBeGreaterThanOrEqual(heroBox.y - 1);
expect(introBox.y + introBox.height).toBeLessThanOrEqual(
heroBox.y + heroBox.height + 1,
);
// Body below the hero.
expect(contentBox.y).toBeGreaterThanOrEqual(heroBox.y + heroBox.height - 1);
// Intro fits within the mobile viewport. // Intro fits within the mobile viewport.
const introInner = intro.locator(".content-wrapper").first(); expect(introBox.x + introBox.width).toBeLessThanOrEqual(402 + 3);
const introInnerBox = await getBox(introInner);
expect(introInnerBox.x + introInnerBox.width).toBeLessThanOrEqual(
402 + 3,
);
// No JS errors. // No JS errors.
expect(pageErrors).toEqual([]); expect(pageErrors).toEqual([]);
@@ -103,7 +112,7 @@ test.describe("inner page layout", () => {
await page.goto(pageUrl); await page.goto(pageUrl);
const hero = page.locator(".page-hero").first(); const hero = page.locator(".page-hero").first();
const intro = page.locator(".page-intro").first(); const intro = page.locator(".page-hero__intro").first();
await expect(hero).toBeVisible(); await expect(hero).toBeVisible();
await expect(intro).toBeVisible(); await expect(intro).toBeVisible();
@@ -122,7 +131,7 @@ test.describe("inner page layout", () => {
await page.goto(pageUrl); await page.goto(pageUrl);
const hero = page.locator(".page-hero").first(); const hero = page.locator(".page-hero").first();
const intro = page.locator(".page-intro").first(); const intro = page.locator(".page-hero__intro").first();
await expect(hero).toBeVisible(); await expect(hero).toBeVisible();
await expect(intro).toBeVisible(); await expect(intro).toBeVisible();
@@ -135,7 +144,7 @@ test.describe("inner page layout", () => {
}); });
// Spec line 113: "A page with no `intro` field renders hero + body with no empty intro wrapper." // Spec line 113: "A page with no `intro` field renders hero + body with no empty intro wrapper."
test("no-intro: hero and body render with no .page-intro wrapper", async ({ test("no-intro: hero and body render with no .page-hero__intro wrapper", async ({
page, page,
}) => { }) => {
await page.setViewportSize({ width: 1280, height: 900 }); await page.setViewportSize({ width: 1280, height: 900 });
@@ -147,13 +156,13 @@ test.describe("inner page layout", () => {
await expect(hero).toBeVisible(); await expect(hero).toBeVisible();
await expect(content).toBeVisible(); await expect(content).toBeVisible();
// No empty intro wrapper rendered when the intro field is empty. // No intro wrapper rendered when the intro field is empty.
expect(await page.locator(".page-intro").count()).toBe(0); expect(await page.locator(".page-hero__intro").count()).toBe(0);
// Hero and body still render in the expected order. // Hero and body still render in the expected order.
const heroBox = await getBox(hero); const heroBox = await getBox(hero);
const contentBox = await getBox(content); const contentBox = await getBox(content);
expect(heroBox.y).toBeLessThan(contentBox.y); expect(contentBox.y).toBeGreaterThanOrEqual(heroBox.y + heroBox.height - 1);
// Heading is still inside the hero. // Heading is still inside the hero.
await expect(hero.locator("h1").first()).toBeVisible(); await expect(hero.locator("h1").first()).toBeVisible();
+42 -42
View File
@@ -16,57 +16,57 @@ $heroVector = get_field( 'hero_vector' );
// Fallback for heading // Fallback for heading
if ( ! $heading ) { if ( ! $heading ) {
$heading = getTheTitle(); $heading = getTheTitle();
} }
// Additional logic for dark mode (if needed) // Additional logic for dark mode (if needed)
if ( is_home() || is_single() || is_archive() || is_search() || is_404() ) { if ( is_home() || is_single() || is_archive() || is_search() || is_404() ) {
$isDark = true; $isDark = true;
} }
// Default the wrapper color to the gradient when no background color is set.
// The actual gradient is applied via .page-hero in page-hero.css; this is a
// fallback for non-gradient pages.
$wrapperStyle = $bgColor ? 'background-color: ' . esc_attr( $bgColor ) . ';' : '';
?> ?>
<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 ) . '"' : ''; ?>> <div class="page-hero text-light overflow-hidden <?php echo $isDark ? 'dark' : ''; ?>" style="<?php echo esc_attr( $wrapperStyle ); ?>">
<?php if ( $heroImage ) : ?> <?php if ( $heroImage ) : ?>
<div class="page-hero__media" aria-hidden="true"> <div class="page-hero__media" aria-hidden="true">
<img <img
src="<?php echo esc_url( $heroImage['url'] ); ?>" src="<?php echo esc_url( $heroImage['url'] ); ?>"
alt="" alt=""
loading="lazy" loading="lazy"
role="presentation" role="presentation"
/> />
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if ( $heroVector ) : ?> <?php if ( $heroVector ) : ?>
<img <img
class="page-hero__vector" class="page-hero__vector"
src="<?php echo esc_url( $heroVector['url'] ); ?>" src="<?php echo esc_url( $heroVector['url'] ); ?>"
alt="" alt=""
loading="lazy" loading="lazy"
aria-hidden="true" aria-hidden="true"
role="presentation" role="presentation"
/> />
<?php endif; ?> <?php endif; ?>
<div class="page-hero__content container mx-auto"> <div class="page-hero__content container mx-auto py-12 lg:py-16">
<div id="breadcrumbs"> <div id="breadcrumbs">
<?php Breadcrumbs::render(); ?> <?php Breadcrumbs::render(); ?>
</div> </div>
<div class="sm:text-center lg:items-start lg:text-left content-wrapper"> <h1 class="text-light font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl mt-6 lg:mt-10">
<?php <?php echo wp_kses_post( $heading ); ?>
// Heading </h1>
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">'; <?php
echo wp_kses_post( $heading ); // Intro block — sits inside the hero as a two-column heading + body
echo '</h1>'; // section below the H1. Renders only when the `intro` field is set.
} else { get_template_part( 'views/partials/page-intro' );
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 ); </div>
echo '</span>';
}
?>
</div>
</div>
</div> </div>
+9 -6
View File
@@ -2,8 +2,8 @@
/** /**
* Page Intro Partial * Page Intro Partial
* *
* Renders the centered narrow intro section beneath the page hero. * Renders the intro heading + body inside the page-hero.
* Loaded by page.php when the `intro` ACF field is non-empty. * Loaded by page-hero.php when the `intro` ACF field is non-empty.
* *
* @package CWC * @package CWC
*/ */
@@ -17,10 +17,13 @@ if ( ! $intro ) {
} }
?> ?>
<section class="page-intro py-12 lg:py-20 text-center" aria-label="<?php echo esc_attr__( 'Page introduction' ); ?>"> <div class="page-hero__intro text-light">
<div class="mx-auto max-w-3xl content-wrapper"> <div class="page-hero__intro-inner">
<p class="page-intro__text text-lg lg:text-xl leading-relaxed text-dark"> <h2 class="page-hero__intro-heading text-2xl lg:text-3xl font-quincy font-normal leading-tight">
<?php echo esc_html__( 'Our Work', 'cwc' ); ?>
</h2>
<p class="page-hero__intro-text text-base lg:text-lg leading-relaxed text-light/90">
<?php echo wp_kses_post( $intro ); ?> <?php echo wp_kses_post( $intro ); ?>
</p> </p>
</div> </div>
</section> </div>