Compare commits

..
43 Commits
Author SHA1 Message Date
Keith Solomon 01101ff712 fix: update mobile navigation styles and comment out unused code
Deploy to Dreamhost (dev) / build (push) Successful in 35s
Sync TODOs with Issues / sync_todos (push) Successful in 7s
2026-06-28 20:52:40 -05:00
Keith Solomon 3db0ceaf8d fix: lock body scroll while menu is open, scroll menu internally
Per the user's request:

- Page scroll is locked when the mobile menu is open: add an
  :has() selector that applies overflow: hidden + touch-action: none to
  the <body> when the .nav-main__toggle has aria-expanded=true. Verified
  with mouse wheel and PageDown: window.scrollY stays at 0.

- Menu scrolls internally when its content overflows: switch
  .menu-vdi's 'overflow-hidden' to 'overflow-y: auto' with
  'overscroll-behavior: contain' so scroll gestures feed the menu
  rather than chain-scroll to the body behind it.

- Cap the menu at viewport height instead of growing: change
  'min-height: calc(100vh - var(--hgtHeader))' to
  'max-height: calc(100vh - var(--hgtHeader))' so the menu fills the
  visible space below the header and any overflowing items become
  scrollable inside the menu.
2026-06-28 19:59:10 -05:00
Keith Solomon 380e2d9015 fix: address remaining mobile menu issues
Per the user's feedback, fix the issues still visible in the menu:

1. Hero rendered over the menu: bumped menu z-index from 10 to 100 so it
   always sits above the hero's stacked elements.

2. List markers on items: added explicit list-style: none / padding: 0 on
   the menu <ul> and items to override the base typography rule that sets
   list-style-type: disc and padding: 0 1rem on every <ul>.

3. Blue links: the base typography rule 'a, .link { color: var(--color-bodylinks); }'
   has specificity 0,1,1 and was beating the menu text-white rule. Override
   with color: var(--color-white) !important on menu links and child links
   inside the mobile media query.

4. Full borders with rounded corners on child items: change child item
   border from '2px solid var(--color-secondary); border-radius: 0.5rem'
   to 'border-bottom: 2px solid var(--color-secondary); border-radius: 0'
   so each child gets only a bottom border (matching the user's spec).

5. Menu items now use plain CSS (color/display/padding/font-weight) instead
   of @apply text-white/no-underline to give them explicit override
   specificity against the base styles.

6. Toggle button: changed the SVG width from 2rem to 1.5rem in
   styles/components/site-header.css so the hamburger and X icons render
   at a square 1.5rem x 1.5rem aspect ratio inside the (intentionally)
   rectangular orange button.

Nav-main template unchanged from the previous commit (Contact button +
social icons already in place). Header test still passes - the button box
itself (72x45) is unchanged, only the icon aspect changed.
2026-06-28 18:05:12 -05:00
Keith Solomon 09d42ba2a9 fix: style mobile menu per spec
Per the user's spec, the mobile menu is:
- Dark navy background, white links, 95% viewport width
- Parent items (those with child links) get an orange chevron
- Child items get an orange border; nothing else gets any border
- Bottom of menu has a Contact link styled as a button and social icons

Changes:

1. Menu CSS (styles/navigation/nav-mobile-sliding.css):
   - Switch the menu background from white to bg-cwc-blue-02 (dark navy)
   - Switch menu item text from black to white
   - Hide the existing .menu-vdi__submenu and show the children inline under their
     parent with a 2px orange border, rounded corners, and white text
   - Style the existing chevron SVG inside the parent <button> with the orange
     secondary color so it's visible against the navy background
   - Add a .menu-vdi__footer block (Contact button + social icons) anchored to
     the bottom of the menu panel, shown only when the toggle is aria-expanded
   - Hide .menu-vdi__footer entirely at desktop so it doesn't leak into the
     page DOM and trigger accessibility warnings

2. Menu template (views/components/nav-main.php):
   - Append a .menu-vdi__footer div after the menu items containing a Contact
     link to home_url('/contact/') and the existing social-media partial
   - The footer renders inside <nav> but is positioned via fixed CSS to align
     with the menu panel

Header/toggle button unchanged - the orange/coral rounded button design from
the original mobile-fidelity plan was already correct and is left alone.
2026-06-28 17:32:05 -05:00
Keith Solomon 2399023286 🐞 fix: Adjust social icon styling 2026-06-28 14:11:46 -05:00
Keith Solomon a761c16802 fix: anchor mobile hero vector to the viewport
Move the mobile hero <img> out of the .vector container (which is intentionally wider than the viewport to bleed the desktop SVG) so the mobile img anchors to .heroVector's full-width edges. Set width: 100vw, left: 0, bottom: 6.25rem so the 402-wide mobile SVG distributes the linework across the full hero at the 402 reference composition.
2026-06-28 13:35:35 -05:00
Keith Solomon 93b9ef68cd docs: record mobile vector replacement spec
Capture the per-block approach, what changed in each PHP/CSS file, and the test/geometry updates needed for the new mobile-*-vector.svg files. Mirror the structure used by the prior 2026-06-27 mobile-homepage-fidelity spec.
2026-06-28 13:10:49 -05:00
Keith Solomon 739cb6e6e2 test: wait for contact linework before measuring
The mobile contact linework is loading=lazy and only resolved when scrolled into view. Wait for it to finish loading before the geometry assertions read its bounding rect, matching the pattern already used for the Our Work desktop vector.
2026-06-28 13:09:40 -05:00
Keith Solomon daeb184dd1 fix: use new mobile vector for our work
Replace the clip-path composition of the desktop SVG with a real <img> that references the artist-supplied mobile-work-vector.svg. Drop the CSS-layer trick and the ::before/::after background pieces; the new mobile SVG already covers the composition. Update the linework composition test to assert against the new <img>.
2026-06-28 13:06:01 -05:00
Keith Solomon 8c239e15f6 fix: use new mobile vector for contact block
Render mobile-contact-vector.svg alongside the existing contact linework. CSS hides the desktop SVG and shows the mobile <img> at <=767px so the new 342x187 composition fills the card width. Update the geometry test to assert the new linework ratio (342/187) and the snapshot.
2026-06-28 13:04:55 -05:00
Keith Solomon 14de24351e fix: use new mobile vector for hero
Add hero-vector-mobile.php that renders mobile-hero-vector.svg alongside the existing desktop hero partial. The homepage-hero.css hides the desktop SVG and shows the mobile <img> below 768px while preserving every other mobile rule.
2026-06-28 13:03:58 -05:00
Keith Solomon dbfd5bfb43 fix: use new mobile vector for pull quote
Swap pullquote-mobile-linework.png for the artist-supplied mobile-pullquote-vector.svg at the existing pull-quote__vector--mobile slot. Update the visual snapshots to match the new artwork.
2026-06-28 13:03:18 -05:00
Keith Solomon 31b498115b test: wait for Our Work desktop vector before reading source 2026-06-28 10:03:58 -05:00
Keith Solomon 0a48a1938c fix: align mobile footer navigation 2026-06-27 22:54:17 -05:00
Keith Solomon 20d25e966d fix: match mobile footer design 2026-06-27 22:49:31 -05:00
Keith Solomon 948cc7e368 refactor: harden responsive Our Work artwork 2026-06-27 22:36:46 -05:00
Keith Solomon 79ff6a1408 fix: match mobile Our Work linework 2026-06-27 22:24:38 -05:00
Keith Solomon 6206761573 fix: match mobile Our Work design 2026-06-27 22:01:47 -05:00
Keith Solomon ca8f5c939d fix: preserve reusable contact linework ratio 2026-06-27 21:38:58 -05:00
Keith Solomon f0c5c47297 refactor: isolate mobile home contact styles 2026-06-27 21:34:44 -05:00
Keith Solomon 7c143721b8 fix: refine mobile contact photo crop 2026-06-27 21:22:30 -05:00
Keith Solomon e2901ce710 fix: correct mobile contact image treatment 2026-06-27 21:16:22 -05:00
Keith Solomon 443be4fb89 fix: match mobile contact card design 2026-06-27 20:58:36 -05:00
Keith Solomon d19924e22b fix: scope mobile media text behavior 2026-06-27 20:35:10 -05:00
Keith Solomon 6263a98988 fix: match mobile image text layout 2026-06-27 20:17:30 -05:00
Keith Solomon 4969e4c9ce test: isolate pull quote linework snapshot 2026-06-27 20:05:52 -05:00
Keith Solomon aaa9d28a79 fix: harden mobile pull quote layout 2026-06-27 20:01:02 -05:00
Keith Solomon 7c3c09855d fix: add mobile pull quote artwork 2026-06-27 19:47:06 -05:00
Keith Solomon 32bbf6750b fix: show full mobile pull quote artwork 2026-06-27 19:27:17 -05:00
Keith Solomon 2986cb3ee8 fix: refine mobile pull quote artwork 2026-06-27 19:20:33 -05:00
Keith Solomon 8a601d90cb fix: align mobile pull quote crop 2026-06-27 19:00:12 -05:00
Keith Solomon 445269d291 test: lock mobile pull quote visual fidelity 2026-06-27 18:48:11 -05:00
Keith Solomon 15e832645c fix: scale mobile pull quote proportionally 2026-06-27 18:37:18 -05:00
Keith Solomon 97ebffb70d fix: match mobile pull quote design 2026-06-27 18:18:15 -05:00
Keith Solomon 81785c4aec refactor: clarify mobile hero geometry 2026-06-27 18:06:34 -05:00
Keith Solomon a360b8192a fix: complete mobile carousel and social updates 2026-06-27 18:00:32 -05:00
Keith Solomon f8d60a63a4 fix: scale mobile hero vector responsively 2026-06-27 17:53:00 -05:00
Keith Solomon bdf78d58ac fix: align mobile hero vector crop 2026-06-27 17:46:58 -05:00
Keith Solomon 6d3bb76b17 fix: match mobile header and hero design 2026-06-27 17:38:49 -05:00
Keith Solomon dd063f1df0 docs: plan mobile homepage fidelity corrections 2026-06-27 17:22:01 -05:00
Keith Solomon 088bdd7cc9 docs: specify mobile homepage fidelity corrections 2026-06-27 17:13:25 -05:00
Keith Solomon 4f2116d77b 🔵 other: Remove non-related spec file 2026-06-27 13:28:22 -05:00
Keith Solomon 1ec5b01b3e docs: specify repo name fallback 2026-06-27 13:20:29 -05:00
48 changed files with 3022 additions and 321 deletions
@@ -0,0 +1,916 @@
# Mobile Homepage Fidelity 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:** Make the homepage match the supplied 402px mobile section crops and remain proportional from 320px through 767px without changing layouts at 768px and above.
**Architecture:** Add narrowly scoped mobile overrides to the block or component that owns each visual region. Use the existing WordPress markup and content, with Playwright geometry assertions written before each correction. Treat 402px as the exact baseline, then verify the lower, upper, and desktop boundary widths.
**Tech Stack:** WordPress/PHP, ACF blocks, Tailwind CSS v4 with nested CSS, Playwright, axe-core, PHPCS, Prettier.
---
## File Map
- `tests/mobile-homepage.spec.js`: mobile geometry, ordering, breakpoint, and accessibility regression coverage.
- `styles/components/site-header.css`: hamburger appearance only.
- `views/blocks/homepage-hero/homepage-hero.php`: decorative location-label hook shown in the approved crop.
- `views/blocks/homepage-hero/homepage-hero.css`: hero height, content, CTAs, media crop, and linework.
- `views/blocks/pull-quote/pull-quote.css`: mobile type, block height, and vector crop.
- `views/blocks/media-text/media-text.css`: mobile text-first order, CTA row, collage size, and block spacing.
- `views/blocks/contact-block/contact-block.css`: mobile card dimensions, photo treatment, text, CTA, and linework.
- `views/blocks/our-work/our-work.css`: mobile content, vector density, and contained overlapping images.
- `styles/components/site-footer.css`: mobile footer grid and alignment.
- `static/dist/theme.css`: generated by `npm run build`; do not hand-edit.
## Reference Material
- `notes/header-hero.png` — 402×874.
- `notes/pullquote.png` — 402×361.
- `notes/image-text.png` — 402×964.
- `notes/contact.png` — 343×502.
- `notes/our-work.png` — 402×888.
- `notes/footer.png` — 402×673.
---
### Task 1: Add reusable mobile geometry assertions
**Files:**
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add geometry helpers below the homepage constant**
```js
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);
};
```
- [ ] **Step 2: Replace the current hamburger size test with the approved target-only test**
```js
test("mobile header preserves its layout and uses the designed hamburger", async ({
page,
}) => {
await page.goto(homepage);
const header = await getBox(page.locator(".site-header"));
const toggle = page.locator(".nav-main__toggle");
const toggleBox = await getBox(toggle);
expectNear(header.height, 70, 2);
expectNear(toggleBox.width, 72, 2);
expectNear(toggleBox.height, 45, 2);
await expect(toggle).toHaveCSS(
"background-color",
"oklch(0.6353 0.1751 29.61)",
);
});
```
- [ ] **Step 3: Run the test and verify it fails for the hamburger dimensions**
Run:
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "designed hamburger" --reporter=line
```
Expected: FAIL because the current button is 36×36 rather than approximately 72×45.
---
### Task 2: Correct the hamburger and homepage hero
**Files:**
- Modify: `styles/components/site-header.css:73-96`
- Modify: `views/blocks/homepage-hero/homepage-hero.php:62-70`
- Modify: `views/blocks/homepage-hero/homepage-hero.css:55-114`
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add a failing hero geometry test**
```js
test("mobile hero matches the 402px reference composition", async ({ page }) => {
await page.goto(homepage);
const hero = await getBox(page.locator(".homepage-hero"));
const heading = await getBox(page.locator(".homepage-hero .intro h1"));
const buttons = page.locator(".homepage-hero .reset .button");
const firstButton = await getBox(buttons.nth(0));
const secondButton = await getBox(buttons.nth(1));
expectNear(hero.height, 752, 8);
expectNear(heading.x, 32, 3);
expect(heading.width).toBeGreaterThanOrEqual(330);
expectNear(firstButton.y, secondButton.y, 2);
expectNear(firstButton.width, 148, 4);
expectNear(secondButton.width, 156, 4);
await expect(page.locator(".homepage-hero__location")).toBeVisible();
});
```
- [ ] **Step 2: Run the hero test and verify the current short composition fails**
Run:
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "reference composition" --reporter=line
```
Expected: FAIL because the current hero is about 332px tall and the content/buttons are undersized.
- [ ] **Step 3: Change only the hamburger button declarations inside the existing mobile query**
```css
.site-header .nav-main__toggle {
align-items: center;
background: var(--color-secondary);
border-radius: 1.4rem 0.35rem 1.4rem 0.35rem;
display: inline-flex;
height: 2.8125rem;
justify-content: center;
margin-right: 1rem;
padding: 0.65rem 1rem;
width: 4.5rem;
}
.site-header .nav-main__toggle svg {
height: 1.5rem;
width: 2rem;
}
```
Do not alter the header container, logo, padding, or height rules.
- [ ] **Step 4: Add the decorative location label after `.heroVector`**
```php
<div class="homepage-hero__location" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path d="M12 21s7-5.6 7-12a7 7 0 1 0-14 0c0 6.4 7 12 7 12Z" />
<circle cx="12" cy="9" r="2.5" />
</svg>
<span><?php echo esc_html__( 'Location / Nation Information', 'cwc' ); ?></span>
</div>
```
The label is marked decorative because it reproduces a visual annotation from the approved design rather than page navigation or editorial content.
- [ ] **Step 5: Replace the hero mobile query with a 402px-baseline composition**
```css
.homepage-hero__location {
display: none;
}
@media (max-width: 767px) {
.homepage-hero {
--hgtHero: clamp(37.5rem, 187vw, 47rem);
min-height: var(--hgtHero);
}
.homepage-hero .content-wrapper {
padding-inline: clamp(1.25rem, 8vw, 2rem);
}
.homepage-hero .content-wrapper > div {
max-width: 21.25rem;
padding-top: clamp(3rem, 14vw, 3.75rem);
text-align: left;
}
.homepage-hero .intro h1 {
font-size: clamp(1.75rem, 8vw, 2rem);
letter-spacing: 0;
line-height: 1.24;
margin-bottom: 1.75rem;
}
.homepage-hero .intro h1 strong {
font-size: inherit;
line-height: inherit;
}
.homepage-hero .intro p {
font-size: var(--text-14px);
line-height: 1.3;
margin: 0;
max-width: 18rem;
}
.homepage-hero .reset {
flex-wrap: nowrap;
gap: clamp(0.75rem, 5.5vw, 1.4rem);
justify-content: flex-start;
margin-top: 2rem;
}
.homepage-hero .button {
font-size: var(--text-18px);
min-height: 2.75rem;
min-width: 9.25rem;
padding: 0.65rem 1rem;
}
.homepage-hero .reset x-button:nth-child(2) .button {
min-width: 9.75rem;
}
.homepage-hero .heroVector .vector {
bottom: 6.25rem;
left: 50%;
transform: translateX(-50%);
width: clamp(43rem, 190vw, 48rem);
}
.homepage-hero .heroMedia img,
.homepage-hero .heroMedia video {
object-position: 58% center;
}
.homepage-hero .homepage-hero__location {
align-items: center;
bottom: 5.5rem;
color: var(--color-cwc-blue-03);
display: flex;
font-size: var(--text-14px);
gap: 0.5rem;
left: 2rem;
position: absolute;
z-index: 30;
}
.homepage-hero .homepage-hero__location svg {
fill: none;
height: 1.25rem;
stroke: currentColor;
stroke-width: 1.75;
width: 1.25rem;
}
}
```
- [ ] **Step 6: Build and run the hamburger/hero tests**
```powershell
npm run build
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "designed hamburger|reference composition" --reporter=line
```
Expected: 2 passed.
- [ ] **Step 7: Capture and inspect the 402px header/hero**
```powershell
npx playwright screenshot --viewport-size "402,874" --wait-for-timeout 1000 http://community-works-collaborative.test "$env:TEMP\cwc-header-hero.png"
```
Compare directly with `notes/header-hero.png`. Adjust only the numeric values in the mobile query until heading, buttons, media crop, and vector anchor match.
- [ ] **Step 8: Commit the header and hero correction**
```powershell
git add styles/components/site-header.css views/blocks/homepage-hero/homepage-hero.php views/blocks/homepage-hero/homepage-hero.css tests/mobile-homepage.spec.js
git commit -m "fix: match mobile header and hero design"
```
---
### Task 3: Correct the pull quote
**Files:**
- Modify: `views/blocks/pull-quote/pull-quote.css:47-60`
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add the failing pull-quote test**
```js
test("mobile pull quote matches the reference scale", async ({ page }) => {
await page.goto(homepage);
const quote = await getBox(page.locator(".pull-quote"));
const text = await getBox(page.locator(".pull-quote__text"));
expectNear(quote.height, 361, 6);
expect(text.width).toBeGreaterThanOrEqual(330);
expect(text.height).toBeGreaterThanOrEqual(235);
});
```
- [ ] **Step 2: Run the test and verify it fails**
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "pull quote" --reporter=line
```
Expected: FAIL because the current text and block are substantially shorter than the reference.
- [ ] **Step 3: Replace the mobile pull-quote overrides**
```css
@media (max-width: 767px) {
.pull-quote {
border-bottom-width: 0.75rem;
min-height: clamp(18rem, 89.8vw, 22.5625rem);
}
.pull-quote .pull-quote__vector img {
height: 118%;
object-fit: cover;
object-position: center 42%;
width: 118%;
}
.pull-quote .pull-quote__content {
align-items: center;
display: flex;
min-height: inherit;
padding: 3.5rem 2rem;
}
.pull-quote .pull-quote__text {
font-size: clamp(1.5rem, 6.75vw, 1.7rem);
line-height: 1.18;
width: 100%;
}
}
```
- [ ] **Step 4: Build, test, and compare with the crop**
```powershell
npm run build
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "pull quote" --reporter=line
```
Expected: PASS. Use a 402px screenshot to compare the vector crop and text wrapping with `notes/pullquote.png`.
- [ ] **Step 5: Commit the pull-quote correction**
```powershell
git add views/blocks/pull-quote/pull-quote.css tests/mobile-homepage.spec.js
git commit -m "fix: match mobile pull quote design"
```
---
### Task 4: Reorder and resize the image/text block
**Files:**
- Modify: `views/blocks/media-text/media-text.css`
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add failing order and CTA-row tests**
```js
test("mobile media text places copy and CTAs before the complete collage", async ({
page,
}) => {
await page.goto(homepage);
const block = page.locator(".media-cols").first();
const content = await getBox(block.locator(".content-wrapper"));
const media = await getBox(block.locator(":scope > div").first());
const buttons = block.locator(".reset .button");
const firstButton = await getBox(buttons.nth(0));
const secondButton = await getBox(buttons.nth(1));
expect(content.y).toBeLessThan(media.y);
expectNear(firstButton.y, secondButton.y, 2);
expect(media.width).toBeGreaterThanOrEqual(335);
});
```
- [ ] **Step 2: Run the test and verify it fails for media order**
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "complete collage" --reporter=line
```
Expected: FAIL because the current `order-first` utility places the collage above the copy.
- [ ] **Step 3: Populate the empty block stylesheet with scoped mobile rules**
```css
@media (max-width: 767px) {
.media-cols {
gap: 2.75rem;
margin-bottom: clamp(12rem, 55vw, 14rem);
padding-inline: clamp(1.25rem, 8vw, 2rem);
}
.media-cols > div:first-child {
order: 2;
width: 100%;
}
.media-cols > .content-wrapper {
order: 1;
padding-inline: 0;
}
.media-cols > div:first-child img {
display: block;
height: auto;
width: 100%;
}
.media-cols .content-wrapper > .font-light {
font-size: var(--text-16px);
line-height: 1.45;
}
.media-cols .reset {
flex-wrap: nowrap;
gap: 1.4rem;
justify-content: flex-start;
margin-top: 2rem;
}
.media-cols .reset .button {
font-size: var(--text-18px);
min-height: 2.75rem;
min-width: 0;
padding: 0.65rem 2rem;
}
}
```
- [ ] **Step 4: Build, test, and compare with the reference**
```powershell
npm run build
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "complete collage" --reporter=line
```
Expected: PASS. Compare the full block with `notes/image-text.png`; tune the mobile `margin-bottom` so the contact block starts at the reference distance rather than the current excessive gap.
- [ ] **Step 5: Commit the media-text correction**
```powershell
git add views/blocks/media-text/media-text.css tests/mobile-homepage.spec.js
git commit -m "fix: match mobile image text layout"
```
---
### Task 5: Correct the contact card composition
**Files:**
- Modify: `views/blocks/contact-block/contact-block.css:46-99`
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add a failing contact-card geometry test**
```js
test("mobile contact card matches the reference frame", async ({ page }) => {
await page.goto(homepage);
const card = await getBox(page.locator(".contact-block.home-contact"));
const content = await getBox(
page.locator(".contact-block.home-contact .contact-block__content"),
);
expectNear(card.width, 343, 4);
expectNear(card.height, 502, 8);
expectNear(content.x - card.x, 32, 4);
});
```
- [ ] **Step 2: Run the test and verify it fails**
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "contact card" --reporter=line
```
Expected: FAIL because the current card is wider, shorter, and top-pads the content by seven rem.
- [ ] **Step 3: Replace the contact block mobile overrides**
```css
@media (max-width: 767px) {
.contact-block {
border-bottom-width: 0.75rem;
border-radius: 1rem 1rem 0 0;
min-height: clamp(25rem, 124.9vw, 31.375rem);
}
.contact-block .contact-block__media {
inset: 0;
mask-image: linear-gradient(to right, rgb(0 0 0 / 0.2) 0%, black 68%);
-webkit-mask-image: linear-gradient(to right, rgb(0 0 0 / 0.2) 0%, black 68%);
width: 100%;
}
.contact-block .contact-block__bg-image {
object-fit: cover;
object-position: 70% center;
}
.contact-block .contact-block__linework {
bottom: 0;
height: auto;
left: 0;
max-width: none;
right: auto;
top: auto;
width: 112%;
}
.contact-block .contact-block__content {
min-height: clamp(25rem, 124.9vw, 31.375rem);
padding: 2rem 2rem 2.75rem !important;
position: relative;
z-index: 2;
}
.contact-block .contact-block__text {
font-size: clamp(1.65rem, 7vw, 1.8rem);
line-height: 1.08;
max-width: 17rem !important;
}
.contact-block.home-contact {
bottom: auto;
left: 50%;
margin: 2.5rem auto -3.75rem;
max-width: 21.4375rem;
position: relative;
transform: translateX(-50%);
width: min(85.3vw, 21.4375rem);
}
}
```
- [ ] **Step 4: Build, test, and visually tune photo/vector anchors**
```powershell
npm run build
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "contact card" --reporter=line
```
Expected: PASS. Compare with `notes/contact.png`; adjust only `object-position`, mask stop, and linework width/offset until the photo and lower vector match.
- [ ] **Step 5: Commit the contact card correction**
```powershell
git add views/blocks/contact-block/contact-block.css tests/mobile-homepage.spec.js
git commit -m "fix: match mobile contact card design"
```
---
### Task 6: Correct Our Work vector and image containment
**Files:**
- Modify: `views/blocks/our-work/our-work.css:193-261`
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add failing block and image-containment tests**
```js
test("mobile Our Work contains both reference images", async ({ page }) => {
await page.goto(homepage);
const block = await getBox(page.locator(".our-work"));
const left = await getBox(page.locator(".our-work__media--left"));
const bottom = await getBox(page.locator(".our-work__media--bottom"));
expectNear(block.height, 888, 12);
expectNear(left.width, 189, 5);
expect(bottom.width).toBeGreaterThanOrEqual(335);
expect(bottom.x).toBeGreaterThanOrEqual(60);
});
```
- [ ] **Step 2: Run the test and verify it fails**
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "contains both" --reporter=line
```
Expected: FAIL because the existing left image is about 152px wide and the bottom image is undersized/clipped.
- [ ] **Step 3: Change the mobile breakpoint to the approved 767px scope and replace its rules**
```css
@media (max-width: 767px) {
.our-work {
margin-bottom: 2rem;
min-height: clamp(44rem, 220.9vw, 55.5rem);
padding-top: 4.25rem;
}
.our-work__vector img {
height: 118%;
object-fit: cover;
object-position: 56% center;
width: 118%;
}
.our-work__inner {
display: block;
padding-inline: 2rem;
}
.our-work__content {
margin-inline: 0;
max-width: 21rem;
text-align: left;
}
.our-work__content .acf-innerblocks-container {
gap: 1.75rem;
}
.our-work__content h1,
.our-work__content h2,
.our-work__content h3,
.our-work__content h4,
.our-work__content h5,
.our-work__content h6 {
font-size: clamp(2.5rem, 11vw, 2.75rem);
}
.our-work__content p {
font-size: var(--text-16px);
line-height: 1.45;
}
.our-work__content .wp-block-acf-buttons > div {
flex-wrap: nowrap;
gap: 2rem;
justify-content: flex-start;
}
.our-work__content .button {
font-size: var(--text-18px);
min-height: 2.75rem;
min-width: 9.75rem;
padding: 0.65rem 1rem;
}
.our-work__media--left {
margin: 4rem 0 0;
max-width: 11.8125rem;
position: relative;
width: 55%;
z-index: 2;
}
.our-work__media--bottom {
margin: -3.5rem -2rem 0 auto;
max-width: none;
position: relative;
width: 100%;
z-index: 1;
}
.our-work__image {
object-fit: contain;
}
}
```
- [ ] **Step 4: Build, test, and compare the overlap**
```powershell
npm run build
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "contains both" --reporter=line
```
Expected: PASS. Compare with `notes/our-work.png`; tune vector `object-position` and the two media margins without changing image containment.
- [ ] **Step 5: Commit the Our Work correction**
```powershell
git add views/blocks/our-work/our-work.css tests/mobile-homepage.spec.js
git commit -m "fix: match mobile Our Work design"
```
---
### Task 7: Rebuild the mobile footer hierarchy
**Files:**
- Modify: `styles/components/site-footer.css:50-80`
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add a failing footer geometry test**
```js
test("mobile footer matches the reference hierarchy", async ({ page }) => {
await page.goto(homepage);
const footer = page.locator(".site-footer");
const footerBox = await getBox(footer);
const logo = await getBox(footer.locator(".site-footer__logo-image"));
const backToTop = await getBox(footer.locator("#backToTopBtn"));
const nav = await getBox(footer.locator("#footer-nav"));
const contact = await getBox(footer.locator(".contact"));
expectNear(footerBox.height, 673, 12);
expectNear(logo.y, backToTop.y, 8);
expect(nav.y).toBeGreaterThan(logo.y + logo.height);
expect(contact.y).toBeGreaterThan(nav.y + nav.height);
});
```
- [ ] **Step 2: Run the test and verify the current four-cell grid fails**
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "footer matches" --reporter=line
```
Expected: FAIL because the back-to-top, nav, contact details, social links, and copyright do not occupy the reference rows.
- [ ] **Step 3: Replace the mobile footer query**
```css
@media (max-width: 767px) {
.site-footer {
min-height: clamp(34rem, 167.4vw, 42.0625rem);
padding-block: 4rem 2rem;
}
.site-footer > .container {
display: grid;
gap: 1.75rem 1rem;
grid-template-columns: minmax(0, 1fr) auto;
padding-inline: 2rem;
}
.site-footer > .container > div:first-child {
grid-column: 1;
grid-row: 1;
}
.site-footer #footer-header {
margin: 0;
max-width: 12.25rem;
}
.site-footer #footer-nav {
grid-column: 1 / -1;
grid-row: 2;
}
.site-footer .site-footer__nav-list {
display: grid;
gap: 0.75rem 2rem;
grid-auto-flow: column;
grid-template-rows: repeat(3, auto);
}
.site-footer #footer-nav + div {
grid-column: 1 / -1;
grid-row: 3;
}
.site-footer #footer-description {
font-size: var(--text-20px);
max-width: 21rem;
}
.site-footer .contact__item {
font-size: var(--text-16px);
}
.site-footer .text-right {
display: contents;
}
.site-footer x-back-to-top {
align-self: start;
grid-column: 2;
grid-row: 1;
}
.site-footer .social-links {
grid-column: 1 / -1;
grid-row: 4;
justify-content: flex-end;
margin-top: 0;
}
.site-footer .copyright {
grid-column: 1 / -1;
grid-row: 5;
padding-block: 0;
text-align: center;
}
}
```
- [ ] **Step 4: Build, test, and compare against the footer crop**
```powershell
npm run build
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "footer matches" --reporter=line
```
Expected: PASS. Compare with `notes/footer.png`; tune only gaps, logo width, and row padding while retaining WordPress-driven content and link order.
- [ ] **Step 5: Commit the footer correction**
```powershell
git add styles/components/site-footer.css tests/mobile-homepage.spec.js
git commit -m "fix: match mobile footer design"
```
---
### Task 8: Verify responsive boundaries and desktop isolation
**Files:**
- Modify: `tests/mobile-homepage.spec.js`
- [ ] **Step 1: Add a parameterized proportional-layout test**
```js
for (const width of [320, 402, 767]) {
test(`homepage remains contained at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.goto(homepage);
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - window.innerWidth,
);
expect(overflow).toBeLessThanOrEqual(1);
await expect(page.locator(".homepage-hero .reset")).toBeVisible();
await expect(page.locator(".contact-block.home-contact")).toBeVisible();
await expect(page.locator(".our-work__media--bottom")).toBeVisible();
});
}
```
- [ ] **Step 2: Add the desktop-isolation assertion**
```js
test("mobile overrides stop below the 768px desktop boundary", async ({
page,
}) => {
await page.setViewportSize({ width: 768, height: 900 });
await page.goto(homepage);
const media = page.locator(".media-cols").first();
const content = await getBox(media.locator(".content-wrapper"));
const image = await getBox(media.locator(":scope > div").first());
expect(image.y).toBeLessThanOrEqual(content.y);
await expect(page.locator(".site-footer > .container")).toHaveCSS(
"grid-template-columns",
/.+ .+ .+ .+/,
);
});
```
- [ ] **Step 3: Run the new boundary tests**
```powershell
npx playwright test tests/mobile-homepage.spec.js --project=chromium --grep "remains contained|desktop boundary" --reporter=line
```
Expected: 4 passed and no horizontal overflow.
- [ ] **Step 4: Run the full production and quality gate**
```powershell
npm run build
npx playwright test --project=chromium --reporter=line
npx --yes prettier@3.6.2 --check static/js/modules/RecentPostsCarousel.js tests/mobile-homepage.spec.js tests/site-a11y.spec.js
composer validate --no-check-publish
vendor\bin\phpcs.bat --standard=.phpcs.xml footer.php views/blocks/recent-posts/recent-posts.php views/icons/facebook.php views/icons/linkedin.php views/icons/youtube.php
php -l footer.php
php -l views/blocks/recent-posts/recent-posts.php
git diff --check
```
Expected: build succeeds; all Playwright/axe tests pass; Prettier, Composer, PHPCS, PHP syntax, and whitespace checks exit 0.
- [ ] **Step 5: Capture final comparison screenshots**
```powershell
npx playwright screenshot --viewport-size "402,874" --full-page --wait-for-timeout 1500 http://community-works-collaborative.test "$env:TEMP\cwc-homepage-402-final.png"
npx playwright screenshot --viewport-size "320,800" --full-page --wait-for-timeout 1500 http://community-works-collaborative.test "$env:TEMP\cwc-homepage-320-final.png"
npx playwright screenshot --viewport-size "767,900" --full-page --wait-for-timeout 1500 http://community-works-collaborative.test "$env:TEMP\cwc-homepage-767-final.png"
```
Inspect the 402px capture section by section against all six reference crops. Inspect 320px and 767px for clipping, awkward line wrapping, and unintended overlap.
- [ ] **Step 6: Commit the boundary tests and final tuning**
```powershell
git add tests/mobile-homepage.spec.js styles/components/site-header.css styles/components/site-footer.css views/blocks/homepage-hero/homepage-hero.css views/blocks/pull-quote/pull-quote.css views/blocks/media-text/media-text.css views/blocks/contact-block/contact-block.css views/blocks/our-work/our-work.css
git commit -m "test: verify mobile homepage responsive fidelity"
```
@@ -0,0 +1,88 @@
# Mobile Homepage Fidelity Corrections
## Goal
Correct the mobile homepage so the supplied section crops in `notes/` are pixel-accurate at a 402px viewport and adapt proportionally from 320px through 767px. Preserve all layouts at 768px and above.
## Responsive Architecture
- Scope presentation changes to `@media (max-width: 767px)`.
- Treat 402px as the exact visual baseline.
- Scale between 320px and 767px with bounded percentages, viewport-relative dimensions, `clamp()`, and component container widths.
- Keep responsive rules with the component or block that owns them.
- Reuse the existing templates and WordPress content. Do not duplicate mobile markup.
- Make minimal template changes only when CSS cannot provide correct semantic ordering or accessible controls.
## Component Corrections
### Header
Change only the hamburger button to match the reference: shape, size, coral fill, and white line treatment. Preserve the current header height, logo treatment, padding, and spacing.
### Homepage Hero
- Match the reference heading width, line breaks, left alignment, and vertical placement.
- Match the CTA dimensions, spacing, and alignment.
- Adjust the background image crop and overlay so the image remains visible behind the content.
- Scale and anchor the linework vector to the bottom composition shown in the reference.
- Position and size the location label to match the crop.
### Pull Quote
- Match the reference type size, weight, line height, and centered text width.
- Match the vertical padding and overall block height.
- Scale and position the background vector independently from the text.
### Image/Text Block
- Present the text before the image collage on mobile.
- Keep the Services and About CTAs side by side.
- Place the complete image collage below the text and CTAs.
- Reduce excess space between the collage and the contact block while retaining the reference breathing room.
### Contact Block
- Match the reference card width, height, corner radius, and bottom coral rule.
- Preserve recognizable photo detail with the reference gradient treatment.
- Match heading width, line breaks, alignment, and CTA placement.
- Scale and anchor the linework along the lower portion of the card.
### Our Work
- Match the reference content-first mobile layout and CTA row.
- Scale the decorative vector independently so its linework matches the reference density.
- Fully contain both supplied images at mobile sizes.
- Recreate their reference overlap without clipping either image.
### Footer
- Match the reference mobile structure: logo and back-to-top row, two-column navigation, full-width contact text/details, social row, and centered copyright.
- Match reference widths, gaps, type scale, icon sizing, and bottom coral border.
- Continue sourcing navigation, contact information, social links, and copyright text from WordPress.
## Content and Behavior
- Do not change editorial content, links, or WordPress field sources.
- Preserve existing desktop and tablet-above-767 behavior.
- Preserve the current accessible Recent Insights carousel behavior.
- Maintain keyboard navigation, focus visibility, semantic reading order, and sufficient touch targets.
## Verification
- Add Playwright regression coverage at 402px for content order, CTA layout, and key component geometry.
- Check proportional behavior at 320px and 767px.
- Add assertions at 768px or wider that guard against mobile overrides leaking into desktop.
- Run the production CSS build.
- Run all configured Playwright and axe accessibility tests.
- Run PHPCS, PHP syntax checks, Prettier, Composer validation, and `git diff --check`.
- Capture a final 402px full-page screenshot and compare it directly with the supplied section crops.
## Acceptance Criteria
1. At 402px, each listed section visually matches its corresponding crop in `notes/`.
2. Between 320px and 767px, content remains proportional, readable, unclipped, and operable.
3. At 768px and above, the existing layout is unchanged.
4. The image/text block uses text-first order, a side-by-side CTA row, and a complete collage below.
5. The contact and Our Work vectors and images match the intended crop and containment behavior.
6. The footer follows the reference hierarchy and alignment.
7. All configured verification commands pass without accessibility regressions.
@@ -0,0 +1,51 @@
# Mobile Vector Replacement
## Goal
Swap the existing mobile vector sources in the homepage hero, pull quote, contact block, and our-work block for the artist-supplied `static/img/mobile-*-vector.svg` files. Each new SVG is drawn at the 402px mobile baseline so it doesn't need a desktop-vector clip-path trick to look right.
## Approach
- Treat each block separately. Same desktop element, an additional mobile element, CSS show/hide.
- Preserve all desktop and tablet-above-767 behavior.
- Lazy-load the new `<img>` elements the same way the desktop sources are loaded.
## Per Block
### Hero
- New partial `views/partials/hero-vector-mobile.php` renders an `<img class="hero-vector--mobile">` referencing `static/img/mobile-hero-vector.svg`.
- `homepage-hero.php` loads both partials inside the existing `.heroVector .vector` container.
- `homepage-hero.css` hides the desktop SVG and shows the mobile `<img>` below 768px. The existing vector positioning rules still apply to the wrapper.
### Pull Quote
- `pull-quote.php` swaps the existing PNG source for `static/img/mobile-pullquote-vector.svg` at the existing `.pull-quote__vector--mobile` slot.
- CSS unchanged. The element already had `display: none` at desktop and `object-fit: cover` at mobile.
### Contact Block
- `contact-block.php` adds an `<img class="contact-block__linework contact-block__linework--mobile">` next to the existing SVG partial.
- `contact-block.css` hides the desktop SVG and shows the mobile `<img>` at mobile. The mobile `<img>` fills the card width (the new SVG is 342×187, ratio matches the card).
- The reusable contact linework ratio assertion updates from `628/282` to `342/187`.
- The linework geometry assertion updates to expect `y ≈ 299`, `height ≈ 188`, `width = card.width`.
### Our Work
- `our-work.php` replaces the clip-path composition (CSS `--our-work-vector-image` + `::before`/`::after` background images) with a real `<img>` referencing `static/img/mobile-work-vector.svg`.
- `our-work.css` drops the `vector-mobile-clip` rules and the two `::before/::after` background layers. Mobile vector is now a single image covering the block.
- The linework composition test updates to assert against the `<img>` (visible, has the right `currentSrc`) instead of the CSS background-image layers.
## Verification
- `npm run build` then `npx playwright test --reporter=line` — 36/36 pass.
- The contact linework test waits for the lazy-loaded `<img>` to finish loading before measuring its bounding rect.
- PHPCS clean on changed files. Pre-existing `lib/activation.php` CRLF error is untouched.
- `git diff --check` clean.
- `composer validate --no-check-publish` passes.
## Acceptance Criteria
1. Below 768px the hero, pull quote, contact block, and our-work block render the artist-supplied mobile vectors.
2. At 768px and above the original desktop SVGs (or PNG for the original pullquote) render unchanged.
3. Existing responsive geometry tests pass with the updated ratios and positions.
+4 -3
View File
@@ -103,14 +103,15 @@ $footerNav = ! empty( $locations['footer_navigation'] )
'views/partials/social-media', 'views/partials/social-media',
null, null,
array( array(
'circle' => true, 'circle' => false,
'classes' => 'social-icons p-0 text-14px bg-white! text-cwc-blue-02! hover:opacity-80!', 'classes' => 'social-icons p-0 hover:opacity-80!',
'size' => '24',
) )
); );
?> ?>
</div> </div>
<div class="copyright font-light text-cwc-blue-03/60 text-right py-3 text-12px"> <div class="copyright font-light text-white/70 text-right py-3 text-12px">
<?php <?php
if ( $copyright ) { if ( $copyright ) {
echo wp_kses_post( $copyright ); echo wp_kses_post( $copyright );
+61 -60
View File
@@ -1,5 +1,5 @@
// @ts-check // @ts-check
import { defineConfig, devices } from '@playwright/test'; import { defineConfig, devices } from "@playwright/test";
/** /**
* Read environment variables from file. * Read environment variables from file.
@@ -13,70 +13,71 @@ import { defineConfig, devices } from '@playwright/test';
* @see https://playwright.dev/docs/test-configuration * @see https://playwright.dev/docs/test-configuration
*/ */
export default defineConfig({ export default defineConfig({
testDir: './tests', testDir: "./tests",
/* Run tests in files in parallel */ snapshotPathTemplate:
fullyParallel: true, "{testDir}/{testFilePath}-snapshots/{arg}{-projectName}{ext}",
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Run tests in files in parallel */
forbidOnly: !!process.env.CI, fullyParallel: true,
/* Retry on CI only */ /* Fail the build on CI if you accidentally left test.only in the source code. */
retries: process.env.CI ? 2 : 0, forbidOnly: !!process.env.CI,
/* Opt out of parallel tests on CI. */ /* Retry on CI only */
workers: process.env.CI ? 1 : undefined, retries: process.env.CI ? 2 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Opt out of parallel tests on CI. */
reporter: 'html', workers: process.env.CI ? 1 : undefined,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
use: { reporter: "html",
/* Base URL to use in actions like `await page.goto('/')`. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
// baseURL: 'http://127.0.0.1:3000', use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: "on-first-retry",
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
}, },
// { /* Configure projects for major browsers */
// name: 'firefox', projects: [
// use: { ...devices['Desktop Firefox'] }, {
// }, name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// { // {
// name: 'webkit', // name: 'firefox',
// use: { ...devices['Desktop Safari'] }, // use: { ...devices['Desktop Firefox'] },
// }, // },
/* Test against mobile viewports. */ // {
// { // name: 'webkit',
// name: 'Mobile Chrome', // use: { ...devices['Desktop Safari'] },
// use: { ...devices['Pixel 5'] }, // },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */ /* Test against mobile viewports. */
// { // {
// name: 'Microsoft Edge', // name: 'Mobile Chrome',
// use: { ...devices['Desktop Edge'], channel: 'msedge' }, // use: { ...devices['Pixel 5'] },
// }, // },
// { // {
// name: 'Google Chrome', // name: 'Mobile Safari',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // use: { ...devices['iPhone 12'] },
// }, // },
],
/* Run your local dev server before starting the tests */ /* Test against branded browsers. */
// webServer: { // {
// command: 'npm run start', // name: 'Microsoft Edge',
// url: 'http://127.0.0.1:3000', // use: { ...devices['Desktop Edge'], channel: 'msedge' },
// reuseExistingServer: !process.env.CI, // },
// }, // {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
}); });
+3
View File
@@ -0,0 +1,3 @@
<svg width="342" height="187" viewBox="0 0 342 187" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-112 188.238C-86.7777 187.909 -56.5316 188.154 -45.5621 174.033C-21.7039 143.302 10.2073 160.42 -12.0704 181.201C-20.575 189.14 -38.5345 191.122 -51.4514 189.76C-64.3683 188.398 -94.351 179.473 -76.495 150.611C-65.2903 132.507 -34.4891 144.429 -34.4891 144.429C-34.5362 126.391 -22.9552 119.692 -14.6481 120.745C-6.10581 121.825 -0.828042 130.929 -0.828042 130.929C10.0474 122.332 12.3241 133.437 12.3241 133.437C14.6666 130.957 27.5177 122.126 40.6321 126.626C62.383 134.095 46.7472 161.772 46.7472 161.772L62.0349 156.85C84.7924 147.849 49.8106 128.063 61.1846 118.415C69.7175 111.171 94.9434 116.319 102.281 109.781C113.458 99.8219 52.5129 109.114 46.5672 96.0357C42.0326 86.077 149.095 85.1281 148.822 94.9459C148.596 103.091 116.807 103.091 124.352 110.325C131.747 117.419 140.966 114.544 144.889 122.389C148.624 129.848 95.3479 128.12 111.661 135.964C149.367 154.097 231.987 151.87 286.58 159.208C312.536 162.693 342.613 164.835 366.857 175.63C387.629 184.884 358.362 190.324 349.424 191.752C249.09 207.864 -32.1936 201.56 20.6217 183.343C50.6703 172.981 111.238 178.806 141.324 174.503C181.947 168.697 69.2883 161.726 105.744 155.421C179.933 142.607 288.669 162.205 266.222 147.013C258.761 141.968 236.493 141.376 246.465 136.397C265.94 126.673 344.617 122.671 359.114 136.942C366.283 143.997 294.323 139.187 306.478 146.139C326.639 157.667 421.526 155.713 399.784 128.89C390.442 117.362 359.82 116.836 344.542 110.814C344.542 110.814 378.372 106.981 394.046 87.7399C397.414 83.6061 397.95 75.47 394.582 70.8665C386.529 59.8555 344.09 59.2261 355.022 78.7677C363.423 93.7903 384.243 71.1765 386.886 64.5718C388.062 61.6406 389.389 58.8879 390.057 55.7687C391.656 48.2809 389.332 41.4507 381.806 38.5383C377.469 36.8566 372.615 36.5747 368.014 36.6593C369.209 32.1215 367.196 27.3488 363.169 25.094C359.321 22.9426 354.429 23.5439 351.155 26.6066C347.881 17.3808 334.974 8.66219 327.589 9.12255C319.62 9.62988 310.26 13.961 312.79 37.1948C312.79 37.1948 301.228 31.1632 292.667 39.7033C280.776 51.5598 292.112 63.6323 292.112 63.6323C292.112 63.6323 279.524 57.4222 274.689 67.6534C271.424 64.7785 269.298 62.8995 264.98 59.0945L263.842 75.8834C285.818 77.5839 315.707 76.1653 337.683 77.8658C334.25 86.9225 326.281 100.292 322.847 109.348C298.058 109.724 273.268 110.1 248.488 110.476C251.517 102.274 254.547 94.0627 257.576 85.8609L222.861 61.34L193.622 59.3294C206.473 70.9322 214.639 74.493 227.49 86.0958C227.49 86.0958 228.271 108.324 227.64 123.563C227.433 128.608 223.162 132.516 218.11 132.3C199.379 131.492 178.296 130.506 160.469 129.773C160.102 129.773 160.102 79.2844 160.102 79.2844L185.616 53.9273L162.962 40.2764C173.611 27.9689 186.961 12.3075 197.601 0L238.459 48.0648C248.987 48.5815 259.514 49.1077 270.041 49.6244C261 37.0821 242.26 17.4559 242.26 17.4559L206.191 10.56C187.958 29.1527 169.726 47.7454 151.503 66.3381M151.503 66.3569C151.503 66.3569 159.989 45.3873 147.081 46.7401C147.081 46.7401 160.977 19.194 135.303 23.8915C135.303 23.8915 131.606 0.216085 115.067 4.41565C98.5277 8.61522 98.3772 25.094 101.096 35.2782C101.096 35.2782 89.3175 29.9513 77.7647 34.7521C66.2119 39.553 60.285 69.0063 93.5792 72.3979C126.883 75.7895 137.26 51.3531 132.584 47.1911C118.087 34.2918 100.927 47.7266 75.4316 65.7556" stroke="#8FC9E6" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 12.0301C24 5.38947 18.624 0 12 0C5.376 0 0 5.38947 0 12.0301C0 17.8526 4.128 22.7008 9.6 23.8195V15.6391H7.2V12.0301H9.6V9.02256C9.6 6.70075 11.484 4.81203 13.8 4.81203H16.8V8.42105H14.4C13.74 8.42105 13.2 8.96241 13.2 9.62406V12.0301H16.8V15.6391H13.2V24C19.26 23.3985 24 18.2737 24 12.0301Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C5.3725 0 0 5.3725 0 12C0 18.6275 5.3725 24 12 24C18.6275 24 24 18.6275 24 12C24 5.3725 18.6275 0 12 0ZM9.0625 16.9738H6.6325V9.15375H9.0625V16.9738ZM7.8325 8.19375C7.065 8.19375 6.56875 7.65 6.56875 6.9775C6.56875 6.29125 7.08 5.76375 7.86375 5.76375C8.6475 5.76375 9.1275 6.29125 9.1425 6.9775C9.1425 7.65 8.6475 8.19375 7.8325 8.19375ZM17.9375 16.9738H15.5075V12.64C15.5075 11.6313 15.155 10.9462 14.2763 10.9462C13.605 10.9462 13.2063 11.41 13.03 11.8563C12.965 12.015 12.9488 12.24 12.9488 12.4638V16.9725H10.5175V11.6475C10.5175 10.6713 10.4862 9.855 10.4537 9.1525H12.565L12.6763 10.2388H12.725C13.045 9.72875 13.8288 8.97625 15.14 8.97625C16.7388 8.97625 17.9375 10.0475 17.9375 12.35V16.9738Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 835 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0037 11.7913L11.1963 10.4813C10.9513 10.3675 10.75 10.495 10.75 10.7662V13.2338C10.75 13.505 10.9513 13.6325 11.1963 13.5188L14.0025 12.2087C14.2488 12.0938 14.2487 11.9063 14.0037 11.7913ZM12 0C5.3725 0 0 5.3725 0 12C0 18.6275 5.3725 24 12 24C18.6275 24 24 18.6275 24 12C24 5.3725 18.6275 0 12 0ZM12 16.875C5.8575 16.875 5.75 16.3213 5.75 12C5.75 7.67875 5.8575 7.125 12 7.125C18.1425 7.125 18.25 7.67875 18.25 12C18.25 16.3213 18.1425 16.875 12 16.875Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

+48
View File
@@ -0,0 +1,48 @@
const mobileViewport = window.matchMedia("(max-width: 640px)");
const initializeCarousel = (carousel) => {
const cards = Array.from(carousel.querySelectorAll(".recent-posts__card"));
const dots = Array.from(carousel.querySelectorAll(".recent-posts__dot"));
const previous = carousel.querySelector(".recent-posts__control--previous");
const next = carousel.querySelector(".recent-posts__control--next");
if (cards.length < 2 || !previous || !next) {
return;
}
let activeIndex = 0;
const render = () => {
cards.forEach((card, index) => {
const isActive = index === activeIndex;
card.classList.toggle("is-active", isActive);
card.hidden = mobileViewport.matches && !isActive;
});
dots.forEach((dot, index) => {
const isActive = index === activeIndex;
dot.classList.toggle("is-active", isActive);
dot.toggleAttribute("aria-current", isActive);
});
};
const showArticle = (index) => {
activeIndex = (index + cards.length) % cards.length;
render();
};
previous.addEventListener("click", () => showArticle(activeIndex - 1));
next.addEventListener("click", () => showArticle(activeIndex + 1));
dots.forEach((dot, index) =>
dot.addEventListener("click", () => showArticle(index)),
);
mobileViewport.addEventListener("change", render);
render();
};
const initRecentPostsCarousels = () => {
document.querySelectorAll(".recent-posts").forEach(initializeCarousel);
};
export default initRecentPostsCarousels;
+4
View File
@@ -7,6 +7,7 @@ import { registerBackToTopButton } from './components/backToTop.js';
import GetHeaderHeight from './modules/GetHeaderHeight.js'; import GetHeaderHeight from './modules/GetHeaderHeight.js';
import tagExternalLinks from './modules/TagExternalLinks.js'; import tagExternalLinks from './modules/TagExternalLinks.js';
import Navigation from './modules/Navigation.js'; import Navigation from './modules/Navigation.js';
import initRecentPostsCarousels from './modules/RecentPostsCarousel.js';
// Add passive event listeners // Add passive event listeners
! function (e) { ! function (e) {
@@ -68,6 +69,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Initialize Header Height // Initialize Header Height
GetHeaderHeight(); GetHeaderHeight();
// Initialize mobile Recent Insights carousels.
initRecentPostsCarousels();
// Add Back to Top button to body // Add Back to Top button to body
// const backToTop = document.createElement('x-back-to-top'); // const backToTop = document.createElement('x-back-to-top');
// document.body.appendChild(backToTop); // document.body.appendChild(backToTop);
+1
View File
@@ -24,6 +24,7 @@
--color-primary-900: color-mix(in oklch, var(--color-primary) 90%, white); --color-primary-900: color-mix(in oklch, var(--color-primary) 90%, white);
--color-secondary: var(--color-cwc-orange-01); --color-secondary: var(--color-cwc-orange-01);
--color-secondary-accessible: #c43c2b;
--color-secondary-100: color-mix(in oklch, var(--color-secondary) 10%, white); --color-secondary-100: color-mix(in oklch, var(--color-secondary) 10%, white);
--color-secondary-200: color-mix(in oklch, var(--color-secondary) 20%, white); --color-secondary-200: color-mix(in oklch, var(--color-secondary) 20%, white);
--color-secondary-300: color-mix(in oklch, var(--color-secondary) 30%, white); --color-secondary-300: color-mix(in oklch, var(--color-secondary) 30%, white);
+4
View File
@@ -1 +1,5 @@
/* Miscellaneous extra styles */ /* Miscellaneous extra styles */
.social-icons svg {
@apply bg-transparent fill-white rounded-full block;
}
+3 -3
View File
@@ -143,10 +143,10 @@ x-button:has(.button[data-button-width="full"]) { @apply w-full; }
* like the ones below and changing the color values. * like the ones below and changing the color values.
*/ */
.btn[data-button-color="secondary"], .button[data-button-color="secondary"] { .btn[data-button-color="secondary"], .button[data-button-color="secondary"] {
--button-bg: var(--color-secondary); --button-bg: var(--color-secondary-accessible);
--button-color: var(--color-white); --button-color: var(--color-white);
--button-border-color: var(--color-secondary); --button-border-color: var(--color-secondary-accessible);
--button-outline-color: var(--color-secondary); --button-outline-color: var(--color-secondary-accessible);
--button-hover-bg: color-mix(in oklch, var(--color-secondary) 74%); --button-hover-bg: color-mix(in oklch, var(--color-secondary) 74%);
--button-hover-border-color: color-mix(in oklch, var(--color-secondary) 74%); --button-hover-border-color: color-mix(in oklch, var(--color-secondary) 74%);
--button-hover-color: var(--color-white); --button-hover-color: var(--color-white);
+137 -31
View File
@@ -1,30 +1,46 @@
/* Footer styles */ /* Footer styles */
.site-footer { .site-footer {
border-bottom: 1rem solid var(--color-orange, #F26B53); border-bottom: 1rem solid var(--color-orange, #f26b53);
a { @apply text-white transition-colors duration-300 hover:text-secondary-400 focus-visible:text-secondary-400; } a {
@apply text-white transition-colors duration-300 hover:text-secondary-400 focus-visible:text-secondary-400;
.widget {
li { @apply text-16px my-1 mb-2 leading-4; }
h4 { @apply font-bold text-18px text-secondary mb-2; }
} }
.menu-footer-menu-container { @apply grow; } .widget {
li {
@apply text-16px my-1 mb-2 leading-4;
}
h4 {
@apply font-bold text-18px text-secondary mb-2;
}
}
.menu-footer-menu-container {
@apply grow;
}
.copyright { .copyright {
p { @apply text-white leading-none m-0 p-0; } p {
@apply text-white leading-none m-0 p-0;
}
a { @apply text-white hover:text-primary-500 underline underline-offset-2; } a {
@apply text-white hover:text-primary-500 underline underline-offset-2;
}
} }
.contact__item { .contact__item {
@apply flex justify-start items-start font-light text-white text-14px mb-2; @apply flex justify-start items-start font-light text-white text-14px mb-2;
p { @apply m-0 p-0; } p {
@apply m-0 p-0;
}
a { @apply text-white hover:text-secondary-400 focus-visible:text-secondary-400; } a {
@apply text-white hover:text-secondary-400 focus-visible:text-secondary-400;
}
&::before { &::before {
@apply inline-block mr-2; @apply inline-block mr-2;
@@ -35,13 +51,16 @@
} }
&.contact__phone::before { &.contact__phone::before {
background: url("../../static/img/footer-phone.svg") no-repeat center center; background: url("../../static/img/footer-phone.svg") no-repeat
center center;
} }
&.contact__email::before { &.contact__email::before {
background: url("../../static/img/footer-email.svg") no-repeat center center; background: url("../../static/img/footer-email.svg") no-repeat
center center;
} }
&.contact__address::before { &.contact__address::before {
background: url("../../static/img/footer-map.svg") no-repeat center center; background: url("../../static/img/footer-map.svg") no-repeat center
center;
padding-top: 2rem; padding-top: 2rem;
} }
} }
@@ -49,32 +68,119 @@
@media (max-width: 767px) { @media (max-width: 767px) {
.site-footer { .site-footer {
padding-block: 2.25rem 1.25rem; border-bottom-width: clamp(4.0625rem, 20.4vw, 5.125rem);
font-size: clamp(0.875rem, 3.98vw, 1rem);
min-height: clamp(34rem, 167.4vw, 42.0625rem);
padding-block: clamp(2.375rem, 11.94vw, 3rem)
clamp(1.125rem, calc(12.2vw - 1.3125rem), 1.75rem);
} }
.site-footer > .container { .site-footer > .container {
gap: 1.5rem 1rem; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 clamp(0.8125rem, 3.98vw, 1rem);
padding-inline: 1rem; grid-template-columns: minmax(0, 1fr) auto;
padding-inline: clamp(1.59375rem, 7.96vw, 2rem);
} }
.site-footer .site-footer__logo { .site-footer > .container > div:first-child {
max-width: 6rem; grid-column: 1;
grid-row: 1;
} }
.site-footer .widget li { .site-footer #footer-header {
font-size: var(--text-14px); margin: 0;
line-height: 1.1; max-width: clamp(9.75rem, 48.76vw, 12.25rem);
margin-bottom: 0.4rem;
} }
.site-footer .widget h4 { .site-footer #footer-nav {
font-size: var(--text-16px); grid-column: 1 / -1;
margin-bottom: 0.5rem; grid-row: 2;
margin-top: clamp(1.5625rem, 7.71vw, 1.9375rem);
} }
.site-footer .text-right { .site-footer .site-footer__nav-list {
align-items: flex-start; display: grid;
text-align: left; gap: clamp(0.625rem, 3.23vw, 0.8125rem) clamp(1.59375rem, 7.96vw, 2rem);
grid-auto-flow: column;
grid-template-rows: repeat(3, auto);
padding-inline-start: 0;
}
.site-footer .site-footer__nav-item {
margin-bottom: 0;
}
.site-footer #footer-nav + div {
grid-column: 1 / -1;
grid-row: 3;
margin-top: clamp(1.25rem, 6.22vw, 1.5625rem);
}
.site-footer #footer-description {
font-size: clamp(1rem, 4.98vw, 1.25rem);
line-height: 1.2;
max-width: 21rem;
}
.site-footer .contact__item {
font-size: clamp(0.75rem, 3.48vw, 0.875rem);
}
.site-footer .contact__address {
margin-top: clamp(0.875rem, 4.48vw, 1.125rem);
}
.site-footer .contact__email,
.site-footer .contact__phone {
margin-top: clamp(0.8125rem, 3.98vw, 1rem);
}
.site-footer .contact__item::before {
height: clamp(1.1875rem, 5.97vw, 1.5rem);
margin-right: clamp(0.375rem, 1.99vw, 0.5rem);
width: clamp(1.1875rem, 5.97vw, 1.5rem);
}
.site-footer .contact__address::before {
padding-top: clamp(1.59375rem, 7.96vw, 2rem);
}
.site-footer > .container > .text-right {
display: contents;
}
.site-footer x-back-to-top {
align-self: start;
grid-column: 2;
grid-row: 1;
}
.site-footer #backToTopBtn {
height: clamp(2rem, 9.95vw, 2.5rem);
padding: 0;
width: clamp(2rem, 9.95vw, 2.5rem);
}
.site-footer .social-links {
grid-column: 1 / -1;
grid-row: 4;
justify-content: flex-end;
margin-top: clamp(0.5rem, 2.49vw, 0.625rem);
}
.site-footer .social-icons {
--size: clamp(1.1875rem, 5.97vw, 1.5rem);
}
.site-footer .social-links a {
margin-right: clamp(0.8125rem, 3.98vw, 1rem);
}
.site-footer .copyright {
grid-column: 1 / -1;
grid-row: 5;
margin-top: clamp(0.8125rem, 4.23vw, 1.0625rem);
padding-block: 0;
text-align: center;
} }
} }
+7 -6
View File
@@ -74,18 +74,19 @@
.site-header .nav-main__toggle { .site-header .nav-main__toggle {
align-items: center; align-items: center;
background: var(--color-secondary); background: var(--color-secondary);
border-radius: 0.375rem; border-radius: 1.4rem 0.35rem 1.4rem 0.35rem;
display: inline-flex; display: inline-flex;
height: 2.25rem; height: 2.8125rem;
justify-content: center; justify-content: center;
margin-block: -0.125rem;
margin-right: 1rem; margin-right: 1rem;
padding: 0.45rem; padding: 0.65rem 1rem;
width: 2.25rem; width: 4.5rem;
} }
.site-header .nav-main__toggle svg { .site-header .nav-main__toggle svg {
height: 1.15rem; height: 1.5rem;
width: 1.15rem; width: 1.5rem;
} }
.site-header .nav-main__toggle svg, .site-header .nav-main__toggle svg,
+2 -2
View File
@@ -29,8 +29,8 @@
/* Mobile Navigation Style - Choose one of the following: */ /* Mobile Navigation Style - Choose one of the following: */
/* Accordion/dropdown style */ /* Accordion/dropdown style */
/* @import "./nav-mobile-accordion.css"; */ @import "./nav-mobile-accordion.css";
/* Sliding viewport style */ /* Sliding viewport style */
@import "./nav-mobile-sliding.css"; /* @import "./nav-mobile-sliding.css"; */
@import "./nav-footer.css"; @import "./nav-footer.css";
+57 -36
View File
@@ -7,46 +7,67 @@
/* Mobile accordion navigation */ /* Mobile accordion navigation */
@media screen and (max-width: 62.5rem) { @media screen and (max-width: 62.5rem) {
.nav-main { .nav-main {
.nav-main__toggle { .nav-main__toggle {
/* display */ /* display */
@apply text-white p-3; @apply text-white p-3;
}
.menu-vdi {
@apply flex-col bg-white w-[95%] right-0 z-10 py-6;
@apply absolute hidden; /* Hidden by default */
top: var(--hgtHeader);
min-height: calc(100vh - var(--hgtHeader));
.menu-vdi__submenu {
@apply py-2 px-7 flex-col;
}
.menu-vdi__item {
a,
button {
/* text */
@apply font-bold text-20px text-black hover:text-light no-underline leading-snug;
/* spacing & display */
@apply block w-full p-4;
/* interaction */
@apply focus-visible:bg-secondary-200 hover:bg-secondary-200;
} }
a { .menu-vdi {
@apply block w-full; @apply relative right-0 py-8 flex flex-col;
@apply absolute hidden; /* Hidden by default */
/* Allow the menu itself to scroll when content overflows the viewport */
overflow-y: auto;
overscroll-behavior: contain;
background: var(--color-cwc-blue-02);
color: var(--color-white);
/* Cap the menu height to the visible viewport area so overflowing items scroll inside the menu instead of letting the menu grow taller than the screen. */
height: calc(100vh - var(--hgtHeader));
top: var(--hgtHeader);
width: 95%;
z-index: 100;
&, ul.menu-vdi {
list-style: none;
padding-left: 0;
margin: 0;
}
.menu-vdi__submenu { @apply py-2 px-7 flex-col; }
.menu-vdi__item {
list-style: none;
a, button {
/* text */
@apply font-bold text-20px text-white hover:underline leading-snug;
/* spacing & display */
@apply block w-full p-4;
/* interaction */
@apply focus-visible:bg-secondary-200 hover:bg-secondary-200;
}
a { @apply block w-full; }
button { @apply flex w-full justify-between; }
}
} }
button { /* Show menu when toggle button is expanded */
@apply flex w-full justify-between; .nav-main__toggle[aria-expanded="true"] ~ .menu-vdi:not(.menu-vdi--sliding) { @apply !flex; /* Use !important to override hidden */ }
}
}
} }
/* Show menu when toggle button is expanded */ /* Lock the page from scrolling while the menu is open so the touch-scroll
.nav-main__toggle[aria-expanded="true"] ~ .menu-vdi:not(.menu-vdi--sliding) { gesture only scrolls the menu (or doesn't scroll at all if the menu
@apply !flex; /* Use !important to override hidden */ fits). Also stop the layout from jumping when the scrollbar disappears. */
html:has(.nav-main__toggle[aria-expanded="true"]) body,
body:has(.nav-main__toggle[aria-expanded="true"]) {
overflow: hidden;
overscroll-behavior: contain;
touch-action: none;
} }
} }
}
+180 -48
View File
@@ -15,11 +15,28 @@
} }
.menu-vdi--sliding { .menu-vdi--sliding {
@apply relative overflow-hidden bg-white w-[95%] right-0 z-10 py-6; @apply relative overflow-hidden right-0 py-8 flex flex-col;
@apply absolute hidden; /* Hidden by default */ @apply absolute hidden; /* Hidden by default */
z-index: 100;
top: var(--hgtHeader); top: var(--hgtHeader);
min-height: calc(100vh - var(--hgtHeader)); min-height: calc(100vh - var(--hgtHeader));
width: 95%;
background: var(--color-cwc-blue-02);
color: var(--color-white);
/* Suppress default <ul> bullets and indent inherited from base typography.
The <ul> element IS .menu-vdi--sliding, so we use & to target the parent selector. */
&,
ul.menu-vdi {
list-style: none;
padding-left: 0;
margin: 0;
}
.menu-vdi__item {
list-style: none;
}
/* Container for all navigation levels */ /* Container for all navigation levels */
.menu-vdi__viewport { .menu-vdi__viewport {
@apply flex transition-transform duration-300 ease-in-out; @apply flex transition-transform duration-300 ease-in-out;
@@ -34,8 +51,8 @@
/* Back button for secondary levels */ /* Back button for secondary levels */
.menu-vdi__back { .menu-vdi__back {
@apply flex items-center gap-2 p-4 border-b border-gray-200 font-bold text-black hover:bg-secondary-200 focus-visible:bg-secondary-200 cursor-pointer; @apply flex items-center gap-2 p-4 border-b border-gray-200 font-bold text-white hover:bg-white/10 focus-visible:bg-white/10 cursor-pointer;
svg { svg {
@apply w-5 h-5; @apply w-5 h-5;
} }
@@ -43,36 +60,102 @@
/* Level indicator for context */ /* Level indicator for context */
.menu-vdi__level-title { .menu-vdi__level-title {
@apply p-4 border-b border-gray-200 font-bold text-lg text-center bg-gray-50; @apply p-4 border-b border-white/20 font-bold text-lg text-center text-white bg-white/10;
} }
/* Navigation items in sliding mode */ /* Top-level menu items */
.menu-vdi__item { .menu-vdi__level--main > .menu-vdi__level-items > .menu-vdi__item {
list-style: none;
a, a,
button { button {
/* text */ /* text */
@apply font-bold text-20px text-black hover:text-light no-underline leading-snug; color: var(--color-white) !important;
font-weight: 700;
font-size: 1.375rem;
line-height: 1.375;
text-decoration: none;
/* spacing & display */ /* spacing & display */
@apply block w-full p-4; display: flex;
align-items: center;
width: 100%;
padding: 1.25rem 1.5rem;
/* interaction */ /* interaction */
@apply focus-visible:bg-secondary-200 hover:bg-secondary-200; background: transparent;
}
a:hover,
a:focus-visible,
button:hover,
button:focus-visible {
background: rgba(255, 255, 255, 0.1);
} }
a { a {
@apply block w-full; justify-content: flex-start;
} }
button { button {
@apply flex w-full justify-between items-center; justify-content: space-between;
}
/* Arrow indicator for items with children */
svg { /* Parent items keep their orange chevron visible */
@apply w-5 h-5; button svg {
fill: none;
stroke: var(--color-secondary);
stroke-width: 2.5;
width: 0.875rem;
height: 0.875rem;
transition: transform 0.2s ease;
}
/* Rotate the chevron when the parent's submenu is expanded */
button[aria-expanded="true"] svg {
transform: rotate(180deg);
}
}
/* Child items: orange bottom border only, no chevron, no rounded corners */
.menu-vdi__submenu {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
.menu-vdi__item--child {
font-weight: 700;
line-height: 1.375;
border-bottom: 2px solid var(--color-secondary);
border-radius: 0;
background: transparent;
list-style: none;
span {
display: block;
width: 100%;
color: var(--color-white);
font-size: 1rem;
padding: 0.75rem 1.5rem;
}
a {
display: block;
width: 100%;
color: var(--color-white) !important;
font-size: 1rem;
font-weight: 700;
padding: 0.75rem 1.5rem;
text-decoration: none;
}
&:hover,
&:focus-visible {
background: rgba(255, 255, 255, 0.1);
} }
} }
} }
/* Hide submenu toggles in sliding mode as they become navigation buttons */ /* Hide traditional submenus in sliding mode as their parent items become navigation buttons */
.menu-vdi__toggle { .menu-vdi__toggle {
/* Override the accordion toggle behavior */ /* Override the accordion toggle behavior */
&[aria-expanded="true"] { &[aria-expanded="true"] {
@@ -86,45 +169,94 @@
} }
} }
/* Hide traditional submenus in sliding mode */
.menu-vdi__submenu {
@apply hidden;
}
/* Ensure menu items in sliding viewport have proper styling */ /* Ensure menu items in sliding viewport have proper styling */
.menu-vdi__level-items { .menu-vdi__level-items {
@apply flex-col; display: flex;
flex-direction: column;
.menu-vdi__item { gap: 0;
@apply w-full; list-style: none;
padding: 0;
a { margin: 0;
@apply p-4 block w-full no-underline;
}
span {
@apply p-4 block w-full font-bold;
}
}
/* Style nested items that were converted from submenus */
.menu-vdi__nested-items {
@apply block;
.menu-vdi__item {
@apply w-full;
a {
@apply p-4 pl-8 block w-full no-underline; /* Add indent for nested items */
}
}
}
} }
} }
/* Footer area: Contact button + social icons below the menu. The footer is a
sibling of .menu-vdi--sliding inside .nav-main, so it cannot be a descendant
selector of the menu. */
.menu-vdi__footer {
display: none;
}
.nav-main__toggle[aria-expanded="true"] ~ .menu-vdi__footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem 1.5rem 1.5rem;
inset: auto 2.5% 0;
position: fixed;
max-width: 95%;
}
.menu-vdi__contact-button {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
text-align: center;
text-decoration: none;
background: var(--color-secondary);
border-radius: 1.25rem 0.25rem;
color: var(--color-white);
padding: 0.75rem 2.5rem;
width: 100%;
max-width: 20rem;
}
.menu-vdi__contact-button:hover,
.menu-vdi__contact-button:focus-visible {
filter: brightness(1.1);
}
.menu-vdi__social {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.menu-vdi__social-icon {
align-items: center !important;
display: inline-flex !important;
height: 2.75rem !important;
justify-content: center !important;
padding: 0 !important;
width: 2.75rem !important;
}
.menu-vdi__social-icon svg {
fill: var(--color-white);
height: 1.5rem;
width: 1.5rem;
}
.menu-vdi__social-icon:hover,
.menu-vdi__social-icon:focus-visible {
opacity: 0.8;
}
/* Show sliding menu when toggle button is expanded and menu has sliding class */ /* Show sliding menu when toggle button is expanded and menu has sliding class */
.nav-main__toggle[aria-expanded="true"] + .menu-vdi--sliding { .nav-main__toggle[aria-expanded="true"] + .menu-vdi--sliding {
@apply !block; /* Use !important to override hidden */ @apply !block; /* Use !important to override hidden */
} }
} }
}
/* The mobile menu footer (Contact button + social icons) only renders inside
the open mobile menu. Hide it on desktop where it would otherwise leak into
the DOM and trigger accessibility warnings. */
@media screen and (min-width: 62.5rem) {
.menu-vdi__footer {
display: none !important;
}
} }
+800
View File
@@ -0,0 +1,800 @@
import { expect, test } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
const homepage = "http://community-works-collaborative.test/";
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.use({ viewport: { width: 402, height: 874 } });
test("mobile header preserves its layout and uses the designed hamburger", async ({
page,
}) => {
await page.goto(homepage);
const header = await getBox(page.locator(".site-header"));
const logo = await getBox(page.locator(".site-header__logo"));
const toggle = page.locator(".nav-main__toggle").first();
const toggleBox = await getBox(toggle);
const hamburgerIcon = toggle.locator(".nav-toggle-hamburger svg");
const closeIcon = toggle.locator(".nav-toggle-x svg");
expectNear(header.height, 70, 2);
expectNear(logo.x, 16, 2);
expectNear(logo.width, 116, 2);
expectNear(logo.height, 36, 2);
expectNear(toggleBox.width, 72, 2);
expectNear(toggleBox.height, 45, 2);
await expect(toggle).toHaveCSS(
"background-color",
"oklch(0.6353 0.1751 29.61)",
);
await expect(toggle).toHaveCSS("border-radius", "22.4px 5.6px");
await expect(hamburgerIcon).toHaveCSS("fill", "oklch(1 0 0)");
await expect(closeIcon).toHaveCSS("stroke", "oklch(1 0 0)");
});
test("mobile hero matches the 402px reference composition", async ({
page,
}) => {
await page.goto(homepage);
const hero = await getBox(page.locator(".homepage-hero"));
const heading = await getBox(page.locator(".homepage-hero .intro h1"));
const vector = await getBox(
page.locator(".homepage-hero .heroVector .vector"),
);
const buttons = page.locator(".homepage-hero .reset .button");
const firstButton = await getBox(buttons.nth(0));
const secondButton = await getBox(buttons.nth(1));
expectNear(hero.height, 752, 8);
expectNear(heading.x, 32, 3);
expect(heading.width).toBeGreaterThanOrEqual(330);
expectNear(vector.x, -414, 8);
expectNear(firstButton.y, secondButton.y, 2);
expectNear(firstButton.width, 148, 4);
expectNear(secondButton.width, 156, 4);
await expect(page.locator(".homepage-hero__location")).toBeVisible();
});
test("mobile hero CTAs remain fully visible at 320px", async ({ page }) => {
await page.setViewportSize({ width: 320, height: 800 });
await page.goto(homepage);
const vector = await getBox(
page.locator(".homepage-hero .heroVector .vector"),
);
const buttons = page.locator(".homepage-hero .reset .button");
const firstButton = await getBox(buttons.nth(0));
const secondButton = await getBox(buttons.nth(1));
expectNear(firstButton.y, secondButton.y, 2);
expect(secondButton.x + secondButton.width).toBeLessThanOrEqual(320);
expect(vector.x + vector.width).toBeGreaterThanOrEqual(316);
});
test("mobile hero vector covers the 767px composition", async ({ page }) => {
await page.setViewportSize({ width: 767, height: 900 });
await page.goto(homepage);
const vector = await getBox(
page.locator(".homepage-hero .heroVector .vector"),
);
expect(vector.x).toBeLessThan(0);
expect(vector.x + vector.width).toBeGreaterThanOrEqual(760);
});
test("mobile pull quote matches the reference scale", async ({ page }) => {
await page.goto(homepage);
const quote = await getBox(page.locator(".pull-quote"));
const text = await getBox(page.locator(".pull-quote__text"));
const desktopVector = page.locator(".pull-quote__vector--bg");
const mobileVector = page.locator(".pull-quote__vector--mobile");
const vector = await getBox(mobileVector);
const bandHeight = await page
.locator(".pull-quote")
.evaluate((element) =>
Number.parseFloat(getComputedStyle(element, "::after").height),
);
expectNear(quote.height, 361, 6);
expectNear(bandHeight, 16, 1);
expectNear(text.x - quote.x, 32, 3);
expect(text.width).toBeGreaterThanOrEqual(330);
expectNear(text.height, 216, 6);
expectNear(
text.y + text.height / 2,
quote.y + (quote.height - bandHeight) / 2,
8,
);
expectNear(vector.x, quote.x, 3);
expectNear(vector.width, quote.width, 3);
expectNear(vector.height, quote.height - bandHeight, 3);
await expect(mobileVector).toBeVisible();
await expect(desktopVector).toBeHidden();
await expect(mobileVector).toHaveCSS("object-fit", "cover");
});
test("mobile pull quote scales proportionally through 767px", async ({
page,
}) => {
await page.setViewportSize({ width: 767, height: 900 });
await page.goto(homepage);
const quote = await getBox(page.locator(".pull-quote"));
const text = await getBox(page.locator(".pull-quote__text"));
expectNear(quote.height, 767 * 0.898, 6);
expect(text.y).toBeGreaterThanOrEqual(quote.y);
expect(text.y + text.height).toBeLessThanOrEqual(quote.y + quote.height);
expectNear(text.y + text.height / 2, quote.y + (quote.height - 16) / 2, 8);
});
test("mobile pull quote keeps text above the band at 320px", async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 800 });
await page.goto(homepage);
const quote = await getBox(page.locator(".pull-quote"));
const content = await getBox(page.locator(".pull-quote__content"));
const text = await getBox(page.locator(".pull-quote__text"));
expectNear(quote.height, 320 * 0.898, 6);
expectNear(content.height, quote.height - 16, 3);
expect(text.y - content.y).toBeGreaterThanOrEqual(12);
expect(
content.y + content.height - (text.y + text.height),
).toBeGreaterThanOrEqual(12);
});
test("mobile pull quote grows when its text needs more room", async ({
page,
}) => {
await page.goto(homepage);
const quoteLocator = page.locator(".pull-quote");
const initialQuote = await getBox(quoteLocator);
await page.locator(".pull-quote__text p").evaluate((paragraph) => {
paragraph.textContent = `${paragraph.textContent} ${paragraph.textContent} ${paragraph.textContent}`;
});
const quote = await getBox(quoteLocator);
const content = await getBox(page.locator(".pull-quote__content"));
const text = await getBox(page.locator(".pull-quote__text"));
const vector = await getBox(page.locator(".pull-quote__vector--mobile"));
expect(quote.height).toBeGreaterThan(initialQuote.height);
expectNear(content.height, quote.height - 16, 3);
expectNear(vector.height, quote.height - 16, 3);
expect(text.y).toBeGreaterThanOrEqual(content.y);
expect(text.y + text.height).toBeLessThanOrEqual(
content.y + content.height,
);
});
test("pull quote restores the configured artwork at 768px", async ({
page,
}) => {
await page.setViewportSize({ width: 768, height: 900 });
await page.goto(homepage);
const desktopVector = page.locator(".pull-quote__vector--bg");
const mobileVector = page.locator(".pull-quote__vector--mobile");
const quote = await getBox(page.locator(".pull-quote"));
const vector = await getBox(desktopVector);
const borderWidth = await page
.locator(".pull-quote")
.evaluate((element) =>
Number.parseFloat(getComputedStyle(element).borderBottomWidth),
);
await expect(desktopVector).toBeVisible();
await expect(mobileVector).toBeHidden();
expectNear(vector.x, quote.x, 3);
expectNear(vector.width, quote.width, 3);
expectNear(vector.height, quote.height - borderWidth, 3);
await expect(desktopVector).toHaveCSS("object-fit", "cover");
});
test("mobile pull quote matches the supplied visual reference", async ({
page,
}) => {
await page.goto(homepage);
await expect(page.locator(".pull-quote")).toHaveScreenshot(
"pullquote-reference.png",
{
animations: "disabled",
maxDiffPixelRatio: 0.071,
threshold: 0.05,
},
);
});
test("mobile pull quote linework render remains stable", async ({ page }) => {
await page.goto(homepage);
await page.locator(".pull-quote__content").evaluate((content) => {
content.style.visibility = "hidden";
});
await expect(page.locator(".pull-quote__vector--mobile")).toHaveScreenshot(
"pullquote-linework-render.png",
{
animations: "disabled",
maxDiffPixelRatio: 0.001,
threshold: 0.02,
},
);
});
test("mobile media text matches the 402px reference composition", async ({
page,
}) => {
await page.goto(homepage);
const blockLocator = page.locator(
".wp-block-acf-media-text.media-cols.has-two-ctas",
);
const block = await getBox(blockLocator);
const content = await getBox(
blockLocator.locator(":scope > .content-wrapper"),
);
const media = await getBox(blockLocator.locator(":scope > .media-wrapper"));
const buttons = blockLocator.locator(".reset .button");
const services = await getBox(buttons.nth(0));
const about = await getBox(buttons.nth(1));
const contact = await getBox(page.locator(".contact-block.home-contact"));
expect(content.y).toBeLessThan(media.y);
expectNear(content.y - block.y, 80, 4);
expectNear(services.y, about.y, 2);
expectNear(services.width, 148, 4);
expectNear(about.width, 124, 4);
expectNear(media.x, 32, 3);
expectNear(media.y - block.y, 458, 4);
expectNear(media.width, 338, 3);
expectNear(media.height, 244, 4);
expectNear(contact.y - (media.y + media.height), 232, 5);
});
test("media text keeps content first in the DOM and marks its CTA pair", async ({
page,
}) => {
await page.goto(homepage);
const block = page.locator(".wp-block-acf-media-text.media-cols");
const childOrder = await block
.locator(":scope > div")
.evaluateAll((children) =>
children.map((child) =>
child.classList.contains("content-wrapper")
? "content"
: "media",
),
);
expect(childOrder).toEqual(["content", "media"]);
await expect(block).toHaveClass(/\bhas-two-ctas\b/);
});
test("mobile media text remains contained at responsive endpoints", async ({
page,
}) => {
for (const width of [320, 767]) {
await page.setViewportSize({ width, height: 900 });
await page.goto(homepage);
const block = page.locator(
".wp-block-acf-media-text.media-cols.has-two-ctas",
);
const content = await getBox(
block.locator(":scope > .content-wrapper"),
);
const media = await getBox(block.locator(":scope > .media-wrapper"));
const buttons = block.locator(".reset .button");
const services = await getBox(buttons.nth(0));
const about = await getBox(buttons.nth(1));
expect(content.y).toBeLessThan(media.y);
expectNear(services.y, about.y, 2);
expect(media.x).toBeGreaterThanOrEqual(0);
expect(media.x + media.width).toBeLessThanOrEqual(width);
expect(services.x).toBeGreaterThanOrEqual(content.x);
expect(about.x + about.width).toBeLessThanOrEqual(
content.x + content.width,
);
}
});
test("media text keeps its configured ordering at 768px", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 900 });
await page.goto(homepage);
const block = page.locator(".wp-block-acf-media-text.media-cols");
const contentLocator = block.locator(":scope > .content-wrapper");
const mediaLocator = block.locator(":scope > .media-wrapper");
const content = await getBox(contentLocator);
const media = await getBox(mediaLocator);
expect(media.y).toBeLessThan(content.y);
expectNear(media.x, 70, 3);
expectNear(media.width, 627, 3);
await expect(block).toHaveCSS("padding-top", "0px");
await expect(contentLocator.locator(".font-light")).toHaveCSS(
"font-size",
"18px",
);
await expect(contentLocator.locator(".reset")).toHaveCSS("display", "flex");
});
test("mobile contact card matches the 402px reference geometry", async ({
page,
}) => {
await page.goto(homepage);
const cardLocator = page.locator(".contact-block.home-contact");
const card = await getBox(cardLocator);
const heading = await getBox(cardLocator.locator(".contact-block__text"));
const cta = await getBox(cardLocator.locator(".contact-block__cta"));
const linework = await getBox(
cardLocator.locator(".contact-block__linework--mobile"),
);
await cardLocator
.locator(".contact-block__linework--mobile")
.evaluate(
(image) =>
/** @type {HTMLImageElement} */ (image).complete ||
new Promise((resolve) => {
/** @type {HTMLImageElement} */ (image).addEventListener(
"load",
() => resolve(undefined),
{ once: true },
);
}),
);
const backgroundImage = cardLocator.locator(".contact-block__bg-image");
const bandHeight = await cardLocator.evaluate((element) =>
Number.parseFloat(getComputedStyle(element).borderBottomWidth),
);
expectNear(card.width, 343, 3);
expectNear(card.height, 502, 5);
expectNear(card.x, (402 - card.width) / 2, 2);
expectNear(bandHeight, 16, 1);
expectNear(heading.x - card.x, 33, 3);
expectNear(heading.y - card.y, 31, 4);
expectNear(cta.x - card.x, 33, 3);
expectNear(cta.y - card.y, 219, 5);
expectNear(cta.width, 173, 4);
expectNear(linework.y - card.y, 299, 5);
expectNear(linework.height, 188, 5);
expectNear(linework.width, card.width, 3);
await expect(backgroundImage).toHaveCSS("object-fit", "cover");
const overlay = await cardLocator
.locator(".contact-block__media")
.evaluate(
(element) => getComputedStyle(element, "::after").backgroundImage,
);
expect(overlay).toContain("linear-gradient");
await expect(cardLocator).toHaveCSS("border-radius", "16px 16px 0px 0px");
const reusableStyles = await cardLocator.evaluate((element) => {
const clone = element.cloneNode(true);
clone.classList.remove("home-contact");
document.body.append(clone);
const media = clone.querySelector(".contact-block__media");
const content = clone.querySelector(".contact-block__content");
const linework = clone.querySelector(
".contact-block__linework--mobile",
);
const lineworkBox = linework.getBoundingClientRect();
const styles = {
borderRadius: getComputedStyle(clone).borderRadius,
contentPaddingLeft: getComputedStyle(content).paddingLeft,
lineworkRatio: lineworkBox.width / lineworkBox.height,
maskImage: getComputedStyle(media).maskImage,
overlay: getComputedStyle(media, "::after").backgroundImage,
};
clone.remove();
return styles;
});
expect(reusableStyles.borderRadius).toBe("12px 12px 0px 0px");
expect(reusableStyles.contentPaddingLeft).toBe("24px");
expectNear(reusableStyles.lineworkRatio, 342 / 187, 0.03);
expect(reusableStyles.maskImage).toContain("linear-gradient");
expect(reusableStyles.overlay).toBe("none");
});
test("mobile contact card scales proportionally and remains contained", async ({
page,
}) => {
for (const width of [320, 767]) {
await page.setViewportSize({ width, height: 900 });
await page.goto(homepage);
const cardLocator = page.locator(".contact-block.home-contact");
const card = await getBox(cardLocator);
const heading = await getBox(
cardLocator.locator(".contact-block__text"),
);
expect(card.x).toBeGreaterThanOrEqual(0);
expect(card.x + card.width).toBeLessThanOrEqual(width);
expectNear(card.width / width, 343 / 402, 0.015);
expectNear(card.height / card.width, 502 / 343, 0.025);
expectNear((heading.x - card.x) / card.width, 33 / 343, 0.01);
}
});
test("contact card restores the desktop layout at 768px", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 900 });
await page.goto(homepage);
const card = page.locator(".contact-block.home-contact");
await expect(card).toHaveCSS("position", "absolute");
await expect(card).toHaveCSS("border-bottom-width", "16px");
await expect(card).toHaveCSS("border-radius", "24px 24px 0px 0px");
});
test("mobile contact card render remains stable", async ({ page }) => {
await page.goto(homepage);
await expect(page.locator(".contact-block.home-contact")).toHaveScreenshot(
"contact-card-render.png",
{
animations: "disabled",
maxDiffPixelRatio: 0.025,
threshold: 0.08,
},
);
});
test("mobile Our Work contains both reference images", async ({ page }) => {
await page.goto(homepage);
const block = await getBox(page.locator(".our-work"));
const left = await getBox(page.locator(".our-work__media--left"));
const bottom = await getBox(page.locator(".our-work__media--bottom"));
expectNear(block.height, 888, 12);
expectNear(left.width, 189, 5);
expect(bottom.width).toBeGreaterThanOrEqual(335);
expect(bottom.x).toBeGreaterThanOrEqual(60);
expect(bottom.x + bottom.width).toBeLessThanOrEqual(402);
});
test("mobile Our Work uses its dedicated linework composition", async ({
page,
}) => {
await page.goto(homepage);
const mobileVector = page.locator(".our-work__vector-mobile");
const mobileImage = mobileVector.locator("img");
await expect(mobileVector).toBeVisible();
await expect(mobileImage).toBeVisible();
await expect(page.locator(".our-work__vector-desktop")).toBeHidden();
await mobileImage.evaluate(
(image) =>
/** @type {HTMLImageElement} */ (image).complete ||
new Promise((resolve) => {
/** @type {HTMLImageElement} */ (image).addEventListener(
"load",
() => resolve(undefined),
{ once: true },
);
}),
);
expect(
await mobileImage.evaluate(
(image) => /** @type {HTMLImageElement} */ (image).currentSrc,
),
).toContain("mobile-work-vector.svg");
});
test("mobile Our Work reserves reference height only for both images", async ({
page,
}) => {
await page.goto(homepage);
const minHeights = await page.locator(".our-work").evaluate((block) => {
const measureVariant = (removedClasses) => {
const clone = block.cloneNode(true);
clone.classList.remove(...removedClasses);
if (removedClasses.includes("has-left-image")) {
clone.querySelector(".our-work__media--left")?.remove();
}
if (removedClasses.includes("has-bottom-image")) {
clone.querySelector(".our-work__media--bottom")?.remove();
}
document.body.append(clone);
const measurements = {
height: clone.getBoundingClientRect().height,
minHeight: Number.parseFloat(getComputedStyle(clone).minHeight),
};
clone.remove();
return measurements;
};
return {
bothImages: {
height: block.getBoundingClientRect().height,
minHeight: Number.parseFloat(getComputedStyle(block).minHeight),
},
bottomOnly: measureVariant(["has-left-image"]),
leftOnly: measureVariant(["has-bottom-image"]),
noImages: measureVariant(["has-left-image", "has-bottom-image"]),
};
});
expectNear(minHeights.bothImages.minHeight, 888, 3);
expectNear(minHeights.bothImages.height, 888, 12);
for (const variant of [
minHeights.bottomOnly,
minHeights.leftOnly,
minHeights.noImages,
]) {
expect(variant.minHeight).toBe(0);
expect(variant.height).toBeLessThan(minHeights.bothImages.height);
}
});
test("mobile Our Work linework render remains stable", async ({ page }) => {
await page.goto(homepage);
await expect(page.locator(".our-work")).toHaveScreenshot(
"our-work-linework-render.png",
{
animations: "disabled",
maxDiffPixelRatio: 0.025,
threshold: 0.08,
},
);
});
test("mobile Our Work follows the reference content and overlap order", async ({
page,
}) => {
await page.goto(homepage);
const block = await getBox(page.locator(".our-work"));
const content = await getBox(page.locator(".our-work__content"));
const left = await getBox(page.locator(".our-work__media--left"));
const bottom = await getBox(page.locator(".our-work__media--bottom"));
const paragraph = page.locator(".our-work__content p");
const paragraphBox = await getBox(paragraph);
const buttons = page.locator(".our-work .button");
const primary = await getBox(buttons.nth(0));
const secondary = await getBox(buttons.nth(1));
expectNear(content.x - block.x, 32, 3);
expectNear(content.y - block.y, 68, 8);
expectNear(paragraphBox.y - block.y, 146, 4);
expect(content.y).toBeLessThan(left.y);
expectNear(left.y - block.y, 428, 12);
expectNear(bottom.y - block.y, 595, 12);
expectNear(primary.y, secondary.y, 2);
expectNear(primary.y - block.y, 321, 4);
expectNear(primary.height, 44, 2);
expectNear(secondary.height, 44, 2);
await expect(paragraph).toHaveCSS("text-wrap", "wrap");
});
test("mobile Our Work remains contained at responsive endpoints", async ({
page,
}) => {
for (const width of [320, 767]) {
await page.setViewportSize({ width, height: 900 });
await page.goto(homepage);
const block = await getBox(page.locator(".our-work"));
const left = await getBox(page.locator(".our-work__media--left"));
const bottom = await getBox(page.locator(".our-work__media--bottom"));
const buttons = page.locator(".our-work .button");
const primary = await getBox(buttons.nth(0));
const secondary = await getBox(buttons.nth(1));
for (const item of [left, bottom, primary, secondary]) {
expect(item.x).toBeGreaterThanOrEqual(block.x);
expect(item.x + item.width).toBeLessThanOrEqual(
block.x + block.width,
);
}
expectNear(primary.y, secondary.y, 2);
expect(left.y).toBeLessThan(bottom.y);
const mobileImage = page.locator(".our-work__vector-mobile img");
await expect(mobileImage).toBeVisible();
await mobileImage.evaluate(
(image) =>
/** @type {HTMLImageElement} */ (image).complete ||
new Promise((resolve) => {
/** @type {HTMLImageElement} */ (image).addEventListener(
"load",
() => resolve(undefined),
{ once: true },
);
}),
);
}
});
test("Our Work restores its tablet layout at 768px", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 900 });
await page.goto(homepage);
const block = page.locator(".our-work");
const inner = block.locator(".our-work__inner");
const vector = block.locator(".our-work__vector-desktop img");
await expect(block).toHaveCSS("min-height", "0px");
await expect(block).toHaveCSS("padding-top", "64px");
await expect(inner).toHaveCSS("padding-left", "38.4px");
await expect(vector).toHaveCSS("width", "768px");
await expect(block.locator(".our-work__vector-mobile")).toBeHidden();
// Force lazy-loaded image to start fetching before reading its source.
await expect(vector).toBeVisible();
await vector.evaluate(
(image) =>
/** @type {HTMLImageElement} */ (image).complete ||
new Promise((resolve) => {
/** @type {HTMLImageElement} */ (image).addEventListener(
"load",
() => resolve(undefined),
{ once: true },
);
}),
);
expect(
await vector.evaluate(
(image) => /** @type {HTMLImageElement} */ (image).currentSrc,
),
).toContain("our-work-vector.svg");
});
test("mobile recent posts exposes previous and next controls", async ({
page,
}) => {
await page.goto(homepage);
await expect(
page.getByRole("button", { name: "Previous article" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Next article" }),
).toBeVisible();
});
test("mobile recent posts advances to the next article", async ({ page }) => {
await page.goto(homepage);
const cards = page.locator(".recent-posts__card");
await expect(cards.nth(0)).toBeVisible();
await expect(cards.nth(1)).toBeHidden();
await page.getByRole("button", { name: "Next article" }).click();
await expect(cards.nth(0)).toBeHidden();
await expect(cards.nth(1)).toBeVisible();
});
test("configured footer channels use the prepared SVG icons", async ({
page,
}) => {
await page.goto(homepage);
await expect(
page.locator('.social-links img[src*="social-facebook.svg"]'),
).toBeVisible();
await expect(
page.locator('.social-links img[src*="social-linkedin.svg"]'),
).toBeVisible();
});
test("mobile footer matches the reference hierarchy", async ({ page }) => {
await page.goto(homepage);
const footer = page.locator(".site-footer");
const footerBox = await getBox(footer);
const logo = await getBox(footer.locator(".site-footer__logo-image"));
const backToTop = await getBox(footer.locator("#backToTopBtn"));
const nav = await getBox(footer.locator("#footer-nav"));
const firstNavLink = await getBox(
footer.locator(".site-footer__nav-link").first(),
);
const contact = await getBox(footer.locator(".contact"));
const firstContactItem = await getBox(
footer.locator(".contact__item").first(),
);
expectNear(footerBox.height, 673, 12);
expectNear(logo.y, backToTop.y, 8);
expect(nav.y).toBeGreaterThan(logo.y + logo.height);
expect(contact.y).toBeGreaterThan(nav.y + nav.height);
expectNear(firstNavLink.x, logo.x, 2);
expectNear(firstNavLink.x, firstContactItem.x, 2);
});
test("mobile footer scales down and remains contained", async ({ page }) => {
for (const width of [320, 767]) {
await page.setViewportSize({ width, height: 900 });
await page.goto(homepage);
const footer = page.locator(".site-footer");
const footerBox = await getBox(footer);
const logo = await getBox(footer.locator(".site-footer__logo-image"));
const backToTop = await getBox(footer.locator("#backToTopBtn"));
const nav = await getBox(footer.locator("#footer-nav"));
const contact = await getBox(footer.locator(".contact"));
const social = await getBox(footer.locator(".social-links"));
const copyright = await getBox(footer.locator(".copyright"));
const expectedHeight = Math.min(width, 402) * (673 / 402);
expectNear(footerBox.height, expectedHeight, 12);
expectNear(logo.y, backToTop.y, 8);
expect(nav.y).toBeGreaterThan(logo.y + logo.height);
expect(contact.y).toBeGreaterThan(nav.y + nav.height);
expect(social.y).toBeGreaterThan(contact.y + contact.height);
expect(copyright.y).toBeGreaterThan(social.y + social.height);
expect(footerBox.x).toBeGreaterThanOrEqual(0);
expect(footerBox.x + footerBox.width).toBeLessThanOrEqual(width);
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBeLessThanOrEqual(width);
}
});
test("footer restores its four-column desktop layout at 768px", async ({
page,
}) => {
await page.setViewportSize({ width: 768, height: 900 });
await page.goto(homepage);
const footer = page.locator(".site-footer");
const outer = footer.locator(":scope > .container");
await expect(footer).toHaveCSS("border-bottom-width", "16px");
await expect(footer).toHaveCSS("padding-top", "64px");
await expect(outer).toHaveCSS("grid-template-columns", /.+ .+ .+ .+/);
await expect(outer.locator(":scope > .text-right")).toHaveCSS(
"display",
"flex",
);
});
test("mobile homepage has no automated accessibility violations", async ({
page,
}) => {
await page.goto(homepage);
const results = await new AxeBuilder({ page })
.withTags([
"wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"wcag22a",
"wcag22aa",
])
.analyze();
expect(results.violations).toEqual([]);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+67 -39
View File
@@ -1,60 +1,88 @@
const { test, expect } = require('@playwright/test'); const { test, expect } = require("@playwright/test");
const AxeBuilder = require('@axe-core/playwright').default; const AxeBuilder = require("@axe-core/playwright").default;
const domain = 'http://domain.local/'; const domain =
process.env.TEST_BASE_URL || "http://community-works-collaborative.test/";
test.use({ test.use({
viewport: { width: 1920, height: 1080 }, viewport: { width: 1920, height: 1080 },
}); });
test.describe('site-test', () => { test.describe("site-test", () => {
test('Homepage Test', async ({ page }, testInfo) => { test("Homepage Test", async ({ page }, testInfo) => {
await page.goto(domain); await page.goto(domain);
await page.screenshot({ path: 'test-results/homepage.png', fullPage: true }); await page.screenshot({
path: "test-results/homepage.png",
fullPage: true,
});
const accessibilityScanResults = await new AxeBuilder({ page }) const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa']) .withTags([
.analyze(); "wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"wcag22a",
"wcag22aa",
])
.analyze();
await testInfo.attach('accessibility-scan-results', { await testInfo.attach("accessibility-scan-results", {
body: JSON.stringify(accessibilityScanResults, null, 2), body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json' contentType: "application/json",
});
expect(accessibilityScanResults.violations).toEqual([]);
}); });
expect(accessibilityScanResults.violations).toEqual([]); test("Blog Index Page Test", async ({ page }, testInfo) => {
}); await page.goto(`${domain}news/`);
test('Blog Index Page Test', async ({ page }, testInfo) => { await page.screenshot({
await page.goto(`${domain}news/`); path: "test-results/blog-index.png",
fullPage: true,
});
await page.screenshot({ path: 'test-results/blog-index.png', fullPage: true }); const accessibilityScanResults = await new AxeBuilder({ page })
.withTags([
"wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"wcag22a",
"wcag22aa",
])
.analyze();
const accessibilityScanResults = await new AxeBuilder({ page }) await testInfo.attach("accessibility-scan-results", {
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa']) body: JSON.stringify(accessibilityScanResults, null, 2),
.analyze(); contentType: "application/json",
});
await testInfo.attach('accessibility-scan-results', { expect(accessibilityScanResults.violations).toEqual([]);
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json'
}); });
expect(accessibilityScanResults.violations).toEqual([]); test("404 Page Test", async ({ page }, testInfo) => {
}); await page.goto(`${domain}yaya/`);
test('404 Page Test', async ({ page }, testInfo) => { await page.screenshot({ path: "test-results/404.png", fullPage: true });
await page.goto(`${domain}yaya/`);
await page.screenshot({ path: 'test-results/404.png', fullPage: true }); const accessibilityScanResults = await new AxeBuilder({ page })
.withTags([
"wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"wcag22a",
"wcag22aa",
])
.analyze();
const accessibilityScanResults = await new AxeBuilder({ page }) await testInfo.attach("accessibility-scan-results", {
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa']) body: JSON.stringify(accessibilityScanResults, null, 2),
.analyze(); contentType: "application/json",
});
await testInfo.attach('accessibility-scan-results', { expect(accessibilityScanResults.violations).toEqual([]);
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json'
}); });
expect(accessibilityScanResults.violations).toEqual([]);
});
}); });
+104 -7
View File
@@ -2,17 +2,24 @@
.contact-block { .contact-block {
background-color: var(--color-navy, #0d1f35); background-color: var(--color-navy, #0d1f35);
border-bottom: 1rem solid var(--color-orange, #F26B53); border-bottom: 1rem solid var(--color-orange, #f26b53);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
em { color: var(--color-secondary); font-style: normal; } em {
color: var(--color-secondary);
font-style: normal;
}
.contact-block__media { .contact-block__media {
inset-block: 0; inset-block: 0;
inset-inline-end: 0; inset-inline-end: 0;
mask-image: linear-gradient(to right, transparent 0%, black 30%); mask-image: linear-gradient(to right, transparent 0%, black 30%);
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 30%); -webkit-mask-image: linear-gradient(
to right,
transparent 0%,
black 30%
);
position: absolute; position: absolute;
} }
@@ -30,8 +37,12 @@
width: auto; width: auto;
} }
.contact-block__content { .contact-block__linework--mobile {
display: none;
}
.contact-block__content {
padding-inline: 1rem;
} }
&.home-contact { &.home-contact {
@@ -51,8 +62,16 @@
.contact-block .contact-block__media { .contact-block .contact-block__media {
inset: 0; inset: 0;
mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0.9) 0%, rgb(0 0 0 / 0.45) 100%); mask-image: linear-gradient(
-webkit-mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0.9) 0%, rgb(0 0 0 / 0.45) 100%); to bottom,
rgb(0 0 0 / 0.9) 0%,
rgb(0 0 0 / 0.45) 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
rgb(0 0 0 / 0.9) 0%,
rgb(0 0 0 / 0.45) 100%
);
} }
.contact-block .contact-block__bg-image { .contact-block .contact-block__bg-image {
@@ -60,7 +79,12 @@
} }
.contact-block .contact-block__linework { .contact-block .contact-block__linework {
display: none;
}
.contact-block .contact-block__linework--mobile {
bottom: 0.75rem; bottom: 0.75rem;
display: block;
height: auto; height: auto;
max-width: 112%; max-width: 112%;
right: -32%; right: -32%;
@@ -79,12 +103,85 @@
} }
.contact-block.home-contact { .contact-block.home-contact {
background-color: #032f46;
border-bottom-width: calc(100vw * 16 / 402);
border-radius: calc(100vw * 16 / 402) calc(100vw * 16 / 402) 0 0;
box-sizing: border-box;
bottom: auto; bottom: auto;
height: calc(100vw * 502 / 402);
left: 50%; left: 50%;
margin: 2.5rem auto -3.75rem; margin: 2.5rem auto -3.75rem;
max-width: none; max-width: none;
padding-inline: 0 !important;
padding-top: 0 !important;
position: relative; position: relative;
transform: translateX(-50%); transform: translateX(-50%);
width: min(calc(100vw - 2rem), 22.5rem); width: calc(100vw * 343 / 402);
}
.contact-block.home-contact .contact-block__media {
inset: 0;
mask-image: none;
-webkit-mask-image: none;
}
.contact-block.home-contact .contact-block__media::after {
background: linear-gradient(
to right,
rgb(3 47 70 / 0.98) 0%,
rgb(3 47 70 / 0.96) 40%,
rgb(3 47 70 / 0.8) 52%,
rgb(3 47 70 / 0.1) 72%,
transparent 100%
);
content: "";
inset: 0;
pointer-events: none;
position: absolute;
z-index: 1;
}
.contact-block.home-contact .contact-block__bg-image {
filter: brightness(1.5);
object-fit: cover;
object-position: 82% center;
position: relative;
z-index: 0;
}
.contact-block.home-contact .contact-block__linework {
display: none;
}
.contact-block.home-contact .contact-block__linework--mobile {
bottom: 0;
display: block;
height: auto;
left: 0;
max-width: none;
overflow: visible;
right: auto;
width: 100%;
z-index: 2;
}
.contact-block.home-contact .contact-block__content {
margin: 0 !important;
max-width: none !important;
min-height: 0;
padding: calc(100vw * 31 / 402) calc(100vw * 33 / 402) 0 !important;
width: 100% !important;
z-index: 2;
}
.contact-block.home-contact .contact-block__text {
font-size: calc(100vw * 28 / 402);
line-height: 1.08;
margin-bottom: calc(100vw * 37 / 402);
max-width: calc(100vw * 236 / 402) !important;
}
.contact-block.home-contact .contact-block__cta {
padding-block: calc(100vw * 8.5 / 402);
} }
} }
+3 -1
View File
@@ -31,9 +31,11 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
<?php endif; ?> <?php endif; ?>
<?php get_template_part( '/views/blocks/contact-block/linework' ); ?> <?php get_template_part( '/views/blocks/contact-block/linework' ); ?>
<img class="contact-block__linework contact-block__linework--mobile" src="<?php echo esc_url( get_theme_file_uri( '/static/img/mobile-contact-vector.svg' ) ); ?>" alt="" loading="lazy" role="presentation" />
</div> </div>
<div class="contact-block__content container px-4! pt-12 pb-16 relative z-10"> <div class="contact-block__content container pt-12 pb-16 relative z-10">
<?php if ( $text ) : ?> <?php if ( $text ) : ?>
<h2 class="contact-block__text font-quincy font-normal leading-none text-white max-w-4/5 mb-8"> <h2 class="contact-block__text font-quincy font-normal leading-none text-white max-w-4/5 mb-8">
<?php echo wp_kses_post( $text ); ?> <?php echo wp_kses_post( $text ); ?>
+73 -21
View File
@@ -33,6 +33,10 @@
svg { svg {
} }
.hero-vector--mobile {
display: none;
}
} }
.intro { .intro {
@@ -51,28 +55,38 @@
} }
} }
.homepage-hero__location {
display: none;
}
@media (max-width: 767px) { @media (max-width: 767px) {
.homepage-hero { .homepage-hero {
--hgtHero: 20.75rem; --hgtHero: clamp(37.5rem, 187vw, 47rem);
--hero-content-inset: clamp(1.25rem, 8vw, 2rem);
/* Preserve the 402px crop, then grow the linework to the 767px edge. */
--hero-vector-inline-start: max(-8vw, calc(63.8vw - 18.04rem));
--hero-vector-width: calc(100vw + 22.875rem);
min-height: var(--hgtHero); min-height: var(--hgtHero);
} }
.homepage-hero .content-wrapper {
padding-inline: 1rem;
}
.homepage-hero .content-wrapper > div { .homepage-hero .content-wrapper > div {
max-width: 20rem; margin-left: var(--hero-content-inset);
padding-top: 2.25rem; padding-top: clamp(3rem, 14vw, 3.75rem);
text-align: left; text-align: left;
width: min(
21.25rem,
calc(
100vw - var(--hero-content-inset) - var(--hero-content-inset)
)
);
} }
.homepage-hero .intro h1 { .homepage-hero .intro h1 {
font-size: clamp(1.8rem, 8vw, 2.2rem); font-size: clamp(1.75rem, 8vw, 2rem);
letter-spacing: 0; letter-spacing: 0;
line-height: 0.98; line-height: 1.24;
margin-bottom: 0.75rem; margin-bottom: 1.75rem;
} }
.homepage-hero .intro h1 strong { .homepage-hero .intro h1 strong {
@@ -84,31 +98,69 @@
font-size: var(--text-14px); font-size: var(--text-14px);
line-height: 1.3; line-height: 1.3;
margin: 0; margin: 0;
max-width: 16rem; max-width: 18rem;
} }
.homepage-hero .reset { .homepage-hero .reset {
gap: 0.7rem; flex-wrap: nowrap;
gap: clamp(0.75rem, 5.5vw, 1.4rem);
justify-content: flex-start; justify-content: flex-start;
margin-top: 1rem; margin-top: 2rem;
} }
.homepage-hero .button { .homepage-hero .button {
font-size: var(--text-14px); font-size: var(--text-18px);
min-height: 2.25rem; min-height: 2.75rem;
min-width: 6.35rem; min-width: clamp(7.5rem, 36.82vw, 9.25rem);
padding: 0.5rem 0.8rem; padding: 0.65rem 1rem;
}
.homepage-hero .reset x-button:nth-child(2) .button {
min-width: clamp(7.75rem, 38.8vw, 9.75rem);
} }
.homepage-hero .heroVector .vector { .homepage-hero .heroVector .vector {
bottom: 0.15rem; bottom: 6.25rem;
left: 50%; left: var(--hero-vector-inline-start);
transform: translateX(-50%); transform: translateX(-50%);
width: 170%; width: var(--hero-vector-width);
}
.homepage-hero .heroVector .vector > svg {
display: none;
}
.homepage-hero .heroVector .hero-vector--mobile {
bottom: 6.25rem;
display: block;
height: auto;
left: 0;
position: absolute;
width: 100vw;
} }
.homepage-hero .heroMedia img, .homepage-hero .heroMedia img,
.homepage-hero .heroMedia video { .homepage-hero .heroMedia video {
object-position: center top; object-position: 58% center;
}
.homepage-hero .homepage-hero__location {
align-items: center;
bottom: 5.5rem;
color: var(--color-cwc-blue-03);
display: flex;
font-size: var(--text-14px);
gap: 0.5rem;
left: 2rem;
position: absolute;
z-index: 30;
}
.homepage-hero .homepage-hero__location svg {
fill: none;
height: 1.25rem;
stroke: currentColor;
stroke-width: 1.75;
width: 1.25rem;
} }
} }
@@ -81,8 +81,17 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
<div class="vector absolute bottom-8 w-full"> <div class="vector absolute bottom-8 w-full">
<?php get_template_part( 'views/partials/hero-vector' ); ?> <?php get_template_part( 'views/partials/hero-vector' ); ?>
</div> </div>
<?php get_template_part( 'views/partials/hero-vector-mobile' ); ?>
</div> </div>
<!-- <div class="homepage-hero__location" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path d="M12 21s7-5.6 7-12a7 7 0 1 0-14 0c0 6.4 7 12 7 12Z" />
<circle cx="12" cy="9" r="2.5" />
</svg>
<span><?php echo esc_html__( 'Location / Nation Information', 'cwc' ); ?></span>
</div> -->
<div class="topGradient absolute inset-0 z-1"><!-- Rectangle 88 --></div> <div class="topGradient absolute inset-0 z-1"><!-- Rectangle 88 --></div>
<div class="bgVector absolute inset-0 z-1"><!-- Rectangle 83 --></div> <div class="bgVector absolute inset-0 z-1"><!-- Rectangle 83 --></div>
+42
View File
@@ -0,0 +1,42 @@
@media (max-width: 767px) {
.wp-block-acf-media-text.media-cols {
gap: 3rem;
grid-template-columns: minmax(0, 1fr);
padding: 5rem 0 0;
padding-inline: 0 !important;
}
.wp-block-acf-media-text.media-cols > .media-wrapper {
order: 2;
}
.wp-block-acf-media-text.media-cols > .media-wrapper img {
display: block;
height: auto;
width: 100%;
}
.wp-block-acf-media-text.media-cols > .content-wrapper {
order: 1;
padding: 0;
}
.wp-block-acf-media-text.media-cols > .content-wrapper .font-light {
font-size: 1rem;
line-height: 1.5;
}
.wp-block-acf-media-text.media-cols.has-two-ctas > .content-wrapper .reset {
display: grid;
gap: 1.375rem;
grid-template-columns: minmax(0, 148fr) minmax(0, 124fr);
width: min(100%, 18.375rem);
}
.wp-block-acf-media-text.media-cols.has-two-ctas
> .content-wrapper
.reset
.button {
width: 100%;
}
}
+12 -8
View File
@@ -27,6 +27,10 @@ $mediaClass = $mediaLeft ? 'order-first' : 'order-last';
$contentClass = $mediaLeft ? 'order-last pr-8' : 'order-first pl-8'; $contentClass = $mediaLeft ? 'order-last pr-8' : 'order-first pl-8';
$classes = 'media-cols grid grid-cols-1 lg:grid-cols-2 gap-24 items-center mb-8 p-0 container'; $classes = 'media-cols grid grid-cols-1 lg:grid-cols-2 gap-24 items-center mb-8 p-0 container';
if ( 2 === count( $ctas ) ) {
$classes .= ' has-two-ctas';
}
if ( $isDark ) { if ( $isDark ) {
$classes .= ' dark text-white'; $classes .= ' dark text-white';
} }
@@ -41,14 +45,6 @@ if ( $vidBlock && strpos( $vidBlock, '<iframe' ) !== false ) {
?> ?>
<div <?php echo wp_kses_post( $wrapper ); ?>> <div <?php echo wp_kses_post( $wrapper ); ?>>
<div class="<?php echo esc_attr( $mediaClass ); ?> <?php echo $vidBlock ? 'aspect-video' : ''; ?>">
<?php if ( $vidBlock ) : ?>
<?php echo wp_kses( $vidBlock, escEmbeds() ); ?>
<?php else : ?>
<img src="<?php echo esc_url( $imgBlock['url'] ); ?>" alt="<?php echo esc_attr( $imgBlock['alt'] ); ?>" class="">
<?php endif; ?>
</div>
<div class="content-wrapper text-balance <?php echo esc_attr( $contentClass ); ?>"> <div class="content-wrapper text-balance <?php echo esc_attr( $contentClass ); ?>">
<?php if ( ! empty( $heading ) ) : ?> <?php if ( ! empty( $heading ) ) : ?>
<h2 class=""><?php echo esc_html( $heading ); ?></h2> <h2 class=""><?php echo esc_html( $heading ); ?></h2>
@@ -96,4 +92,12 @@ if ( $vidBlock && strpos( $vidBlock, '<iframe' ) !== false ) {
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="media-wrapper <?php echo esc_attr( $mediaClass ); ?> <?php echo $vidBlock ? 'aspect-video' : ''; ?>">
<?php if ( $vidBlock ) : ?>
<?php echo wp_kses( $vidBlock, escEmbeds() ); ?>
<?php else : ?>
<img src="<?php echo esc_url( $imgBlock['url'] ); ?>" alt="<?php echo esc_attr( $imgBlock['alt'] ); ?>" class="">
<?php endif; ?>
</div>
</div> </div>
+68 -27
View File
@@ -1,13 +1,12 @@
/* Our Work block styles */ /* Our Work block styles */
.our-work { .our-work {
background: background: linear-gradient(
linear-gradient( 90deg,
90deg, var(--color-cwc-blue-01) 0%,
var(--color-cwc-blue-01) 0%, var(--color-cwc-blue-02) 52%,
var(--color-cwc-blue-02) 52%, var(--color-cwc-blue-02) 100%
var(--color-cwc-blue-02) 100% );
);
border-bottom: 16px solid var(--color-secondary); border-bottom: 16px solid var(--color-secondary);
color: var(--color-white); color: var(--color-white);
/* margin-bottom: clamp(4rem, 7vw, 5.5rem); */ /* margin-bottom: clamp(4rem, 7vw, 5.5rem); */
@@ -36,6 +35,16 @@
width: 100%; width: 100%;
} }
.our-work__vector-desktop {
display: block;
height: 100%;
width: 100%;
}
.our-work__vector-mobile {
display: none;
}
.our-work__inner { .our-work__inner {
display: grid; display: grid;
gap: clamp(2rem, 4vw, 4rem) 2.5rem; gap: clamp(2rem, 4vw, 4rem) 2.5rem;
@@ -56,7 +65,7 @@
.our-work__content { .our-work__content {
grid-area: content; grid-area: content;
margin-lefT: 4rem; margin-left: 4rem;
max-width: 46.75rem; max-width: 46.75rem;
} }
@@ -190,23 +199,39 @@
} }
} }
@media (max-width: 640px) { @media (max-width: 767px) {
.our-work { .our-work {
margin-bottom: 2rem; margin-bottom: 2rem;
padding-top: 3.25rem; padding-top: 4.25rem;
}
.our-work.has-left-image.has-bottom-image {
min-height: clamp(44rem, 220.9vw, 55.5rem);
}
.our-work__vector-desktop {
display: none;
}
.our-work__vector-mobile {
display: block;
inset: 0;
position: absolute;
} }
.our-work__inner { .our-work__inner {
gap: 1.4rem; gap: 0;
padding-inline: 2rem !important;
} }
.our-work__content { .our-work__content {
max-width: 20rem; margin-inline: 0;
max-width: 21rem;
text-align: left; text-align: left;
} }
.our-work__content .acf-innerblocks-container { .our-work__content .acf-innerblocks-container {
gap: 1rem; gap: 2.125rem;
} }
.our-work__content h1, .our-work__content h1,
@@ -215,43 +240,59 @@
.our-work__content h4, .our-work__content h4,
.our-work__content h5, .our-work__content h5,
.our-work__content h6 { .our-work__content h6 {
font-size: clamp(2rem, 9vw, 2.45rem); font-size: clamp(2.5rem, 11vw, 2.75rem);
} }
.our-work__content p { .our-work__content p {
font-size: var(--text-14px); font-size: var(--text-16px);
line-height: 1.35; line-height: 1.45;
text-wrap: wrap;
} }
.our-work__content .wp-block-acf-buttons > div { .our-work__content .wp-block-acf-buttons > div {
gap: 0.75rem; flex-wrap: nowrap;
gap: 2rem;
justify-content: flex-start; justify-content: flex-start;
} }
.our-work__content x-button {
flex: 1 1 0;
min-width: 0;
}
.our-work__content .button { .our-work__content .button {
font-size: var(--text-14px); font-size: var(--text-18px);
min-height: 2.25rem; min-height: 2.75rem;
min-width: 6.35rem; min-width: 0;
padding: 0.5rem 0.8rem; padding: 0.65rem 1rem;
white-space: nowrap;
width: 100%;
} }
.our-work__media--left { .our-work__media--left {
justify-self: start; justify-self: start;
margin: 0 0 -3.25rem; margin: 4rem 0 0;
max-width: 9.5rem; max-width: 11.8125rem;
position: relative; position: relative;
width: 55.9%;
z-index: 2; z-index: 2;
} }
.our-work__media--bottom { .our-work__media--bottom {
justify-self: end; justify-self: end;
margin-bottom: -2.35rem; margin: -3.5rem 0 0;
max-width: 20.5rem; max-width: none;
width: calc(100% - 2.25rem); position: relative;
transform: translateX(2rem);
width: 100%;
z-index: 1;
} }
.our-work:not(.has-left-image) .our-work__media--bottom { .our-work:not(.has-left-image) .our-work__media--bottom {
justify-self: center;
width: 100%; width: 100%;
} }
.our-work__image {
object-fit: contain;
}
} }
+21 -8
View File
@@ -9,8 +9,10 @@
namespace CWC; namespace CWC;
$left_image = get_field( 'left_image' ); $left_image = get_field( 'left_image' );
$bottom_image = get_field( 'bottom_image' ); $bottom_image = get_field( 'bottom_image' );
$vector_url = get_stylesheet_directory_uri() . '/views/blocks/our-work/our-work-vector.svg';
$mobile_vector_url = get_theme_file_uri( '/static/img/mobile-work-vector.svg' );
$classes = 'our-work mx-break-out mb-36'; $classes = 'our-work mx-break-out mb-36';
@@ -31,12 +33,23 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
<section <?php echo wp_kses_post( $wrapper ); ?>> <section <?php echo wp_kses_post( $wrapper ); ?>>
<div class="our-work__vector" aria-hidden="true"> <div class="our-work__vector" aria-hidden="true">
<img <div class="our-work__vector-mobile">
src="<?php echo esc_url( get_stylesheet_directory_uri() . '/views/blocks/our-work/our-work-vector.svg' ); ?>" <img
alt="" src="<?php echo esc_url( $mobile_vector_url ); ?>"
loading="lazy" alt=""
role="presentation" loading="lazy"
/> role="presentation"
/>
</div>
<div class="our-work__vector-desktop">
<img
src="<?php echo esc_url( $vector_url ); ?>"
alt=""
loading="lazy"
role="presentation"
/>
</div>
</div> </div>
<div class="our-work__inner container"> <div class="our-work__inner container">
+61 -1
View File
@@ -18,6 +18,10 @@
width: 100%; width: 100%;
} }
.pull-quote__vector .pull-quote__vector--mobile {
display: none;
}
.pull-quote__content { .pull-quote__content {
padding: 5rem 0; padding: 5rem 0;
position: relative; position: relative;
@@ -32,7 +36,8 @@
text-align: center; text-align: center;
text-wrap: balance; text-wrap: balance;
strong, b { strong,
b {
color: var(--color-cwc-blue-02); color: var(--color-cwc-blue-02);
font-weight: 800; font-weight: 800;
} }
@@ -43,3 +48,58 @@
} }
} }
} }
@media (max-width: 767px) {
.pull-quote {
background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, white);
border-bottom: 0;
min-height: calc(89.8vw - 1px);
}
.pull-quote::after {
background: var(--color-cwc-blue-03);
bottom: 0;
content: "";
height: 1rem;
left: 0;
position: absolute;
right: 0;
z-index: 2;
}
.pull-quote .pull-quote__vector {
bottom: 1rem;
}
.pull-quote .pull-quote__vector .pull-quote__vector--bg {
display: none;
}
.pull-quote .pull-quote__vector .pull-quote__vector--mobile {
display: block;
height: 100%;
max-width: none;
object-fit: cover;
width: 100%;
}
.pull-quote .pull-quote__content {
align-items: center;
display: flex;
margin-bottom: 1rem;
min-height: calc(89.8vw - 1rem - 1px);
padding: 2.5rem 2rem;
}
.pull-quote .pull-quote__text {
font-size: clamp(1.25rem, 5.535vw, 1.390625rem);
line-height: clamp(1.5rem, 6.78vw, 1.703125rem);
width: 100%;
}
}
@media (max-width: 360px) {
.pull-quote .pull-quote__content {
padding-block: 0.9375rem;
}
}
+5 -4
View File
@@ -28,11 +28,12 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
?> ?>
<section <?php echo wp_kses_post( $wrapper ); ?>> <section <?php echo wp_kses_post( $wrapper ); ?>>
<?php if ( $quote_bg ) : ?> <div class="pull-quote__vector" aria-hidden="true">
<div class="pull-quote__vector" aria-hidden="true"> <?php if ( $quote_bg ) : ?>
<img class="pull-quote__vector--bg" src="<?php echo esc_url( $quote_bg['url'] ); ?>" alt="" loading="lazy" role="presentation" /> <img class="pull-quote__vector--bg" src="<?php echo esc_url( $quote_bg['url'] ); ?>" alt="" loading="lazy" role="presentation" />
</div> <?php endif; ?>
<?php endif; ?> <img class="pull-quote__vector--mobile" src="<?php echo esc_url( get_theme_file_uri( '/static/img/mobile-pullquote-vector.svg' ) ); ?>" alt="" loading="lazy" role="presentation" />
</div>
<div class="container wider pull-quote__content"> <div class="container wider pull-quote__content">
<div class="pull-quote__text"> <div class="pull-quote__text">
+81 -8
View File
@@ -75,6 +75,11 @@
position: relative; position: relative;
} }
.recent-posts__controls,
.recent-posts__pagination {
display: none;
}
.recent-posts__grid::before { .recent-posts__grid::before {
background: var(--color-cwc-blue-03); background: var(--color-cwc-blue-03);
content: ""; content: "";
@@ -273,10 +278,57 @@
top: min(184px, calc(100cqw * 250 / 397)); top: min(184px, calc(100cqw * 250 / 397));
} }
.recent-posts__card:nth-child(n + 2) { .recent-posts__card {
display: none; display: none;
} }
.recent-posts__card.is-active {
display: flex;
}
.recent-posts__controls {
display: block;
}
.recent-posts__control {
align-items: center;
background: var(--color-secondary);
border: 0;
border-radius: 50%;
color: var(--color-white);
display: flex;
height: 3rem;
justify-content: center;
padding: 0;
position: absolute;
top: 10rem;
width: 3rem;
z-index: 3;
}
.recent-posts__control:hover,
.recent-posts__control:focus-visible {
background: var(--color-secondary-800);
}
.recent-posts__control svg {
fill: none;
height: 1.75rem;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.5;
width: 1.75rem;
}
.recent-posts__control--previous {
left: -1.5rem;
}
.recent-posts__control--next {
right: -1.5rem;
}
.recent-posts__image-link { .recent-posts__image-link {
border-radius: 0.5rem 0.5rem 0 0; border-radius: 0.5rem 0.5rem 0 0;
} }
@@ -303,17 +355,38 @@
.recent-posts__footer { .recent-posts__footer {
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
gap: 1.25rem; margin-top: 1.25rem;
margin-top: 1.4rem;
} }
.recent-posts__footer::before { .recent-posts__pagination {
background: display: flex;
radial-gradient(circle, var(--color-cwc-blue-01) 0 0.2rem, transparent 0.22rem) left center / 0.75rem 0.5rem repeat-x; gap: 0.75rem;
justify-content: center;
margin-top: 1.5rem;
}
.recent-posts__dot {
align-items: center;
background: transparent;
border: 0;
display: flex;
height: 1.5rem;
justify-content: center;
padding: 0;
width: 1.5rem;
}
.recent-posts__dot::before {
background: var(--color-cwc-blue-01);
border-radius: 50%;
content: ""; content: "";
display: block;
height: 0.5rem; height: 0.5rem;
width: 2.25rem; opacity: 0.45;
width: 0.5rem;
}
.recent-posts__dot.is-active::before {
opacity: 1;
} }
.recent-posts__blog-link.button { .recent-posts__blog-link.button {
+31 -2
View File
@@ -56,13 +56,15 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
<?php if ( $recent_posts->have_posts() ) : ?> <?php if ( $recent_posts->have_posts() ) : ?>
<div class="recent-posts__grid"> <div class="recent-posts__grid">
<?php <?php
$post_index = 0;
while ( $recent_posts->have_posts() ) : while ( $recent_posts->have_posts() ) :
$recent_posts->the_post(); $recent_posts->the_post();
$post_title = get_the_title(); $post_title = get_the_title();
$post_permalink = get_permalink(); $post_permalink = get_permalink();
$active_class = 0 === $post_index ? ' is-active' : '';
?> ?>
<article class="recent-posts__card"> <article class="recent-posts__card<?php echo esc_attr( $active_class ); ?>">
<?php /* translators: %s: Post title. */ ?> <?php /* translators: %s: Post title. */ ?>
<a class="recent-posts__image-link" href="<?php echo esc_url( $post_permalink ); ?>" aria-label="<?php echo esc_attr( sprintf( __( 'Continue reading %s', 'cwc' ), $post_title ) ); ?>"> <a class="recent-posts__image-link" href="<?php echo esc_url( $post_permalink ); ?>" aria-label="<?php echo esc_attr( sprintf( __( 'Continue reading %s', 'cwc' ), $post_title ) ); ?>">
<?php if ( has_post_thumbnail() ) : ?> <?php if ( has_post_thumbnail() ) : ?>
@@ -96,7 +98,34 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
</a> </a>
</div> </div>
</article> </article>
<?php endwhile; ?> <?php
++$post_index;
endwhile;
?>
<div class="recent-posts__controls">
<button class="recent-posts__control recent-posts__control--previous" type="button" aria-label="<?php echo esc_attr__( 'Previous article', 'cwc' ); ?>">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M15 5 8 12l7 7" />
</svg>
</button>
<button class="recent-posts__control recent-posts__control--next" type="button" aria-label="<?php echo esc_attr__( 'Next article', 'cwc' ); ?>">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m9 5 7 7-7 7" />
</svg>
</button>
</div>
</div>
<div class="recent-posts__pagination" aria-label="<?php echo esc_attr__( 'Choose an article', 'cwc' ); ?>">
<?php for ( $dot_index = 0; $dot_index < $recent_posts->post_count; ++$dot_index ) : ?>
<button
class="recent-posts__dot<?php echo 0 === $dot_index ? ' is-active' : ''; ?>"
type="button"
aria-label="<?php echo esc_attr( sprintf( /* translators: %d: Article number. */ __( 'Show article %d', 'cwc' ), $dot_index + 1 ) ); ?>"
<?php echo 0 === $dot_index ? ' aria-current="true"' : ''; ?>
></button>
<?php endfor; ?>
</div> </div>
<?php wp_reset_postdata(); ?> <?php wp_reset_postdata(); ?>
+11 -1
View File
@@ -34,7 +34,17 @@ $profile = getFieldValue( 'social_media.profile_name' ) ? getFieldValue( 'social
?> ?>
</nav> </nav>
<?php get_template_part( 'views/partials/social-media' ); ?> <?php
get_template_part(
'views/partials/social-media',
null,
array(
'circle' => false,
'classes' => 'social-icons p-0 hover:opacity-80!',
'size' => '16',
)
);
?>
<span class="font-semibold text-16px"><?php echo esc_html( $profile ); ?></span> <span class="font-semibold text-16px"><?php echo esc_html( $profile ); ?></span>
</div> </div>
+20
View File
@@ -21,4 +21,24 @@ global $views;
$menuItems->render(); $menuItems->render();
} }
?> ?>
<!--
<div class="menu-vdi__footer">
<a class="menu-vdi__contact-button button dark" href="<?php echo esc_url( home_url( '/contact/' ) ); ?>">
<?php echo esc_html__( 'Contact', 'cwc' ); ?>
</a>
<div class="menu-vdi__social">
<?php
get_template_part(
'views/partials/social-media',
null,
array(
'circle' => false,
'classes' => 'menu-vdi__social-icon',
'size' => '20',
)
);
?>
</div>
</div> -->
</nav> </nav>
+6 -1
View File
@@ -1 +1,6 @@
<i class="icon-facebook"></i> <?php $size = $args['size'] ?? '24'; ?>
<svg width="<?php echo esc_attr( $size ); ?>" height="<?php echo esc_attr( $size ); ?>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="facebook-icon-title">
<title id="facebook-icon-title">Facebook</title>
<path d="M24 12.0301C24 5.38947 18.624 0 12 0C5.376 0 0 5.38947 0 12.0301C0 17.8526 4.128 22.7008 9.6 23.8195V15.6391H7.2V12.0301H9.6V9.02256C9.6 6.70075 11.484 4.81203 13.8 4.81203H16.8V8.42105H14.4C13.74 8.42105 13.2 8.96241 13.2 9.62406V12.0301H16.8V15.6391H13.2V24C19.26 23.3985 24 18.2737 24 12.0301Z" />
</svg>
+5 -1
View File
@@ -1 +1,5 @@
<i class="icon-linkedin"></i> <?php $size = $args['size'] ?? '24'; ?>
<svg width="<?php echo esc_attr( $size ); ?>" height="<?php echo esc_attr( $size ); ?>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C5.3725 0 0 5.3725 0 12C0 18.6275 5.3725 24 12 24C18.6275 24 24 18.6275 24 12C24 5.3725 18.6275 0 12 0ZM9.0625 16.9738H6.6325V9.15375H9.0625V16.9738ZM7.8325 8.19375C7.065 8.19375 6.56875 7.65 6.56875 6.9775C6.56875 6.29125 7.08 5.76375 7.86375 5.76375C8.6475 5.76375 9.1275 6.29125 9.1425 6.9775C9.1425 7.65 8.6475 8.19375 7.8325 8.19375ZM17.9375 16.9738H15.5075V12.64C15.5075 11.6313 15.155 10.9462 14.2763 10.9462C13.605 10.9462 13.2063 11.41 13.03 11.8563C12.965 12.015 12.9488 12.24 12.9488 12.4638V16.9725H10.5175V11.6475C10.5175 10.6713 10.4862 9.855 10.4537 9.1525H12.565L12.6763 10.2388H12.725C13.045 9.72875 13.8288 8.97625 15.14 8.97625C16.7388 8.97625 17.9375 10.0475 17.9375 12.35V16.9738Z" />
</svg>
+5 -1
View File
@@ -1 +1,5 @@
<i class="icon-youtube"></i> <?php $size = $args['size'] ?? '24'; ?>
<svg width="<?php echo esc_attr( $size ); ?>" height="<?php echo esc_attr( $size ); ?>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0037 11.7913L11.1963 10.4813C10.9513 10.3675 10.75 10.495 10.75 10.7662V13.2338C10.75 13.505 10.9513 13.6325 11.1963 13.5188L14.0025 12.2087C14.2488 12.0938 14.2487 11.9063 14.0037 11.7913ZM12 0C5.3725 0 0 5.3725 0 12C0 18.6275 5.3725 24 12 24C18.6275 24 24 18.6275 24 12C24 5.3725 18.6275 0 12 0ZM12 16.875C5.8575 16.875 5.75 16.3213 5.75 12C5.75 7.67875 5.8575 7.125 12 7.125C18.1425 7.125 18.25 7.67875 18.25 12C18.25 16.3213 18.1425 16.875 12 16.875Z" />
</svg>
+1
View File
@@ -0,0 +1 @@
<img class="hero-vector--mobile" src="<?php echo esc_url( get_theme_file_uri( '/static/img/mobile-hero-vector.svg' ) ); ?>" alt="" loading="lazy" role="presentation" />
+3 -2
View File
@@ -7,8 +7,9 @@
namespace CWC; namespace CWC;
$classes = $args['classes'] ?? '';
$circle = $args['circle'] ?? ''; $circle = $args['circle'] ?? '';
$classes = $args['classes'] ?? '';
$size = $args['size'] ?? '24';
// Define social media sites and their URLs // Define social media sites and their URLs
$sites = array( $sites = array(
@@ -30,7 +31,7 @@ foreach ( $sites as $name => $url ) {
if ( $url ) { if ( $url ) {
?> ?>
<a href="<?php echo esc_url( $url ); ?>" class="<?php echo esc_attr( $classes ); ?> p-0 leading-none"> <a href="<?php echo esc_url( $url ); ?>" class="<?php echo esc_attr( $classes ); ?> p-0 leading-none">
<?php get_template_part( 'views/icons/' . $name ); ?> <?php get_template_part( 'views/icons/' . $name, null, array( 'size' => $size ) ); ?>
<span class="sr-only bg-white text-black">Visit our <?php echo esc_html( $name ); ?> page</span> <span class="sr-only bg-white text-black">Visit our <?php echo esc_html( $name ); ?> page</span>
</a> </a>
<?php <?php