From 2a1050a5454a9ffc6ec96215cd6ab985bd826050 Mon Sep 17 00:00:00 2001 From: Lamont Crook Date: Thu, 16 Jul 2026 16:07:12 -0600 Subject: [PATCH 1/2] Redesign cards block: large/no-image/sliding/itinerary variants - .cards.large: white promo cards with wordmark image, left-aligned copy, orange pill CTA - .cards (no images): dark navy promo panels with serif heading, centered copy, white pill CTA - .cards.sliding: horizontally scrolling image-overlay teaser carousel with prev/next arrows - .cards.itinerary: white route-map result cards with title, ship meta and orange CTA - Shared image aspect-ratio defaults to portrait (389:533), with route-map diagrams (alt text ending in "route map") kept at their native landscape ratio - Full-bleed section width (removed global 1200px max-width cap) Co-Authored-By: Claude Sonnet 5 --- .gitignore | 2 + blocks/cards/cards.css | 342 +++++++++++++++++++++++++++++++++++++--- blocks/cards/cards.js | 59 ++++++- icons/chevron-left.svg | 4 + icons/chevron-right.svg | 4 + styles/styles.css | 2 - 6 files changed, 389 insertions(+), 24 deletions(-) create mode 100644 icons/chevron-left.svg create mode 100644 icons/chevron-right.svg diff --git a/.gitignore b/.gitignore index 189af75..6f1ddd6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ helix-importer-ui # Ensure these files are tracked !CLAUDE.md !AGENTS.md +.claude-plugin/ +.claude/ diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css index 318d2ae..13cf817 100644 --- a/blocks/cards/cards.css +++ b/blocks/cards/cards.css @@ -3,51 +3,88 @@ margin: 0; padding: 0; display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 24px; + grid-template-columns: 1fr; + gap: 32px; +} + +@media (width >= 600px) { + .cards > ul { + grid-template-columns: repeat(2, 1fr); + } } .cards > ul > li { - border: 1px solid var(--border-color); - background-color: var(--background-color); - border-radius: 8px; overflow: hidden; + border-radius: 20px; + background-color: var(--brand-navy-dark); transition: box-shadow 0.2s ease, transform 0.2s ease; } .cards > ul > li:hover { - box-shadow: 0 4px 16px rgb(0 51 102 / 12%); + box-shadow: 0 8px 24px rgb(0 0 0 / 30%); transform: translateY(-2px); } +.cards .cards-card-image { + line-height: 0; +} + +.cards .cards-card-image img { + width: 100%; + aspect-ratio: 389 / 533; + object-fit: cover; +} + +/* route maps are diagrams, not photos: keep their native (landscape) proportions instead of cropping to portrait */ +.cards .cards-card-image img[alt$="route map" i] { + aspect-ratio: auto; +} + .cards .cards-card-body { - padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 40px; + text-align: center; } -.cards .cards-card-body h3 { - color: var(--nav-bg-color); - margin-top: 0; +.cards .cards-card-body h2 { + margin: 0 0 20px; + color: #fff; + font-family: var(--display-font-family); + font-weight: 400; + font-size: var(--heading-font-size-xl); } -.cards .cards-card-image { - line-height: 0; +.cards .cards-card-body h2 strong { + font-weight: inherit; } -.cards > ul > li img { - width: 100%; - aspect-ratio: 4 / 3; - object-fit: cover; +.cards .cards-card-body p { + margin: 0 0 32px; + max-width: 32em; + color: #fff; + font-size: var(--body-font-size-s); +} + +.cards .cards-card-body p strong { + font-weight: 600; +} + +.cards .cards-card-body .button-wrapper { + margin: 0 auto; } .cards .cards-card-body a.button { - background-color: var(--nav-bg-color); - border-color: var(--nav-bg-color); - color: var(--nav-text-color); + background-color: #fff; + border-color: #fff; + color: var(--brand-navy); + font-weight: 700; } .cards .cards-card-body a.button:hover { - background-color: var(--nav-bg-hover-color); - border-color: var(--nav-bg-hover-color); + background-color: var(--light-color); + border-color: var(--light-color); } /* HAL homepage card variants (Popular Destinations, Why Holland America) */ @@ -82,3 +119,266 @@ .cards.features > ul { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } + +/* sliding: horizontally scrolling carousel of image teasers with overlaid copy */ +.cards.sliding > ul { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + scrollbar-width: none; + gap: 16px; +} + +.cards.sliding > ul::-webkit-scrollbar { + display: none; +} + +/* stylelint-disable-next-line no-descending-specificity -- .sliding and .destinations/.features are mutually exclusive modifiers, never applied together */ +.cards.sliding > ul > li { + position: relative; + flex: 0 0 auto; + width: 62vw; + max-width: 280px; + aspect-ratio: 389 / 533; + scroll-snap-align: start; + background-color: var(--brand-navy-dark); +} + +@media (width >= 600px) { + .cards.sliding > ul > li { + width: 389px; + max-width: none; + } +} + +.cards.sliding .cards-card-image { + position: absolute; + inset: 0; +} + +.cards.sliding .cards-card-image img { + width: 100%; + height: 100%; + aspect-ratio: auto; + object-fit: cover; +} + +.cards.sliding .cards-card-body { + position: absolute; + inset: auto 0 0; + display: block; + padding: 20px; + background: linear-gradient(to top, rgb(0 15 40 / 90%), rgb(0 15 40 / 0%) 75%); + text-align: left; +} + +.cards.sliding .cards-card-body h3 { + margin: 0 0 8px; + color: #fff; + font-size: var(--heading-font-size-s); +} + +.cards.sliding .cards-card-body p { + margin: 0 0 14px; + max-width: none; + color: #fff; + font-size: var(--body-font-size-xs); +} + +.cards.sliding .cards-card-body .button-wrapper { + margin: 0; + text-align: right; +} + +.cards.sliding .cards-card-body a.button { + background-color: transparent; + border-color: transparent; + padding: 0; + color: #fff; + font-size: var(--body-font-size-xs); +} + +.cards.sliding .cards-card-body a.button:hover { + background-color: transparent; + border-color: transparent; + color: #fff; + text-decoration: underline; +} + +.cards.sliding .cards-nav { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 16px; +} + +.cards-nav-button { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border: 1px solid #fff; + border-radius: 50%; + background-color: transparent; + cursor: pointer; +} + +.cards-nav-button .icon { + width: 20px; + height: 20px; +} + +.cards-nav-button:disabled { + cursor: not-allowed; + opacity: 0.35; +} + +/* large: promo cards with a wordmark image, bold copy and a pill CTA */ +.cards.large > ul { + grid-template-columns: 1fr; +} + +@media (width >= 600px) { + .cards.large > ul { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (width >= 900px) { + .cards.large > ul { + grid-template-columns: repeat(3, 1fr); + } +} + +/* stylelint-disable-next-line no-descending-specificity -- .large and .destinations/.features are mutually exclusive modifiers, never applied together */ +.cards.large > ul > li { + display: flex; + flex-direction: column; + background-color: var(--background-color); + box-shadow: 0 8px 24px rgb(2 38 88 / 18%); +} + +.cards.large > ul > li:hover { + box-shadow: 0 12px 32px rgb(2 38 88 / 26%); +} + +.cards.large .cards-card-body { + flex: 1; + align-items: stretch; + padding: 28px 28px 32px; + text-align: left; +} + +.cards.large .cards-card-body h3 { + margin: 0 0 16px; +} + +.cards.large .cards-card-body h3 img { + display: block; + width: auto; + max-width: 100%; + height: auto; + max-height: 60px; +} + +.cards.large .cards-card-body p { + margin: 0 0 28px; + max-width: none; + color: var(--brand-navy); + font-weight: 600; + font-size: var(--body-font-size-m); + line-height: 1.4; +} + +.cards.large .cards-card-body .button-wrapper { + margin: auto 0 0; + text-align: center; +} + +/* stylelint-disable-next-line no-descending-specificity -- .sliding and .large are mutually exclusive modifiers, never applied together */ +.cards.large .cards-card-body a.button { + background-color: var(--accent-color); + border-color: var(--accent-color); + color: #fff; + padding: 0.85em 2.4em; + font-weight: 700; +} + +.cards.large .cards-card-body a.button:hover { + background-color: var(--accent-hover-color); + border-color: var(--accent-hover-color); +} + +/* itinerary: route-map result cards with a title, ship meta and a CTA */ +.cards.itinerary > ul { + grid-template-columns: 1fr; +} + +@media (width >= 600px) { + .cards.itinerary > ul { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (width >= 900px) { + .cards.itinerary > ul { + grid-template-columns: repeat(4, 1fr); + } +} + +/* stylelint-disable-next-line no-descending-specificity -- .itinerary and .large are mutually exclusive modifiers, never applied together */ +.cards.itinerary > ul > li { + display: flex; + flex-direction: column; + border-radius: 12px; + background-color: var(--background-color); + box-shadow: 0 4px 16px rgb(0 0 0 / 12%); +} + +.cards.itinerary > ul > li:hover { + box-shadow: 0 8px 24px rgb(0 0 0 / 18%); +} + +.cards.itinerary .cards-card-body { + flex: 1; + align-items: stretch; + padding: 20px; + text-align: left; +} + +.cards.itinerary .cards-card-body h3 { + margin: 0 0 8px; + color: var(--brand-navy); + font-size: var(--heading-font-size-s); +} + +.cards.itinerary .cards-card-body p { + margin: 0; + max-width: none; + color: var(--brand-navy); + font-size: var(--body-font-size-s); +} + +.cards.itinerary .cards-card-body .cards-meta-label { + color: #6b7785; +} + +.cards.itinerary .cards-card-body .button-wrapper { + margin-top: auto; + padding-top: 20px; + text-align: right; +} + +/* stylelint-disable-next-line no-descending-specificity -- .itinerary and .sliding are mutually exclusive modifiers, never applied together */ +.cards.itinerary .cards-card-body a.button { + background-color: var(--accent-color); + border-color: var(--accent-color); + color: #fff; + font-weight: 700; +} + +.cards.itinerary .cards-card-body a.button:hover { + background-color: var(--accent-hover-color); + border-color: var(--accent-hover-color); +} diff --git a/blocks/cards/cards.js b/blocks/cards/cards.js index 4e6ec93..c2af12d 100644 --- a/blocks/cards/cards.js +++ b/blocks/cards/cards.js @@ -1,6 +1,44 @@ -import { createOptimizedPicture } from '../../scripts/aem.js'; +import { createOptimizedPicture, decorateIcons } from '../../scripts/aem.js'; + +function decorateSlider(block, ul) { + const nav = document.createElement('div'); + nav.className = 'cards-nav'; + nav.innerHTML = ` + + + `; + const prev = nav.querySelector('.cards-nav-prev'); + const next = nav.querySelector('.cards-nav-next'); + + const updateNav = () => { + const max = ul.scrollWidth - ul.clientWidth; + prev.disabled = ul.scrollLeft <= 1; + next.disabled = ul.scrollLeft >= max - 1; + }; + + const scrollByCard = (direction) => { + const card = ul.querySelector('li'); + const gap = parseFloat(getComputedStyle(ul).columnGap) || 0; + const amount = card ? card.getBoundingClientRect().width + gap : ul.clientWidth * 0.8; + ul.scrollBy({ left: direction * amount, behavior: 'smooth' }); + }; + + prev.addEventListener('click', () => scrollByCard(-1)); + next.addEventListener('click', () => scrollByCard(1)); + ul.addEventListener('scroll', updateNav, { passive: true }); + new ResizeObserver(updateNav).observe(ul); + + block.append(nav); + decorateIcons(nav); +} export default function decorate(block) { + const isSliding = block.classList.contains('sliding'); + const isItinerary = block.classList.contains('itinerary'); /* change to ul, li */ const ul = document.createElement('ul'); [...block.children].forEach((row) => { @@ -10,8 +48,27 @@ export default function decorate(block) { if (div.children.length === 1 && div.querySelector('picture')) div.className = 'cards-card-image'; else div.className = 'cards-card-body'; }); + const cta = li.querySelector('.cards-card-body a[href]'); + if (cta) { + cta.classList.add('button'); + cta.closest('p').classList.add('button-wrapper'); + } + if (isItinerary) { + const meta = [...li.querySelectorAll('.cards-card-body p')] + .find((p) => !p.classList.contains('button-wrapper') && p.textContent.includes(':')); + if (meta) { + const [label, value] = meta.textContent.split(':').map((s) => s.trim()); + const labelSpan = document.createElement('span'); + labelSpan.className = 'cards-meta-label'; + labelSpan.textContent = `${label}: `; + const strong = document.createElement('strong'); + strong.textContent = value; + meta.replaceChildren(labelSpan, strong); + } + } ul.append(li); }); ul.querySelectorAll('picture > img').forEach((img) => img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }]))); block.replaceChildren(ul); + if (isSliding) decorateSlider(block, ul); } diff --git a/icons/chevron-left.svg b/icons/chevron-left.svg new file mode 100644 index 0000000..6db9439 --- /dev/null +++ b/icons/chevron-left.svg @@ -0,0 +1,4 @@ + + + diff --git a/icons/chevron-right.svg b/icons/chevron-right.svg new file mode 100644 index 0000000..3ee1264 --- /dev/null +++ b/icons/chevron-right.svg @@ -0,0 +1,4 @@ + + + diff --git a/styles/styles.css b/styles/styles.css index 890a206..c511b10 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -351,8 +351,6 @@ main > .section { } main > .section > div { - max-width: 1200px; - margin: auto; padding: 0 24px; } From ab60fc22457f97636d4445e162768647e9ef23c8 Mon Sep 17 00:00:00 2001 From: Lamont Crook Date: Thu, 16 Jul 2026 16:48:38 -0600 Subject: [PATCH 2/2] Re-trigger aem-psi-check against warmed preview Co-Authored-By: Claude Sonnet 5