diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a0903..49acd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] — Migrate Services patterns to the Core Icon block (LS-3229) + +### Changed + +- Replaced `outermost/icon-block` (third-party "Icon Block" plugin) with WordPress 7.1's native Core Icon block (`core/icon`) across the 3 Services page patterns — `patterns/hero/services-hero.php`, `patterns/sections/services-linked-decisions.php`, `patterns/sections/services-service-clusters.php` — referencing icons from the `lightspeed` collection registered in `ls-plugin`. Final batch of the Core Icon block migration (LS-3229). +- Converted the PHP icon arrays in these files (per-service, per-cluster, per-step) from raw inline SVG to bare `lightspeed/{name}` slugs, and rewrote their loop templates to emit `core/icon` block markup dynamically per iteration, removing the now-unused raw-SVG helper variables. + +See [PR #50](https://github.com/lightspeedwp/ls-theme/pull/50). + +--- + +## [Unreleased] — Build Services page: hero, linked decisions, and service clusters (LS-1598) + +### Added + +- Added `patterns/hero/services-hero.php`: breadcrumb, eyebrow, heading, description, CTAs, and a lifecycle-phase-coloured row of all 14 service tags linking to their `/services/` pages. +- Added `patterns/sections/services-linked-decisions.php`: a six-step process pill row (Discover/Create/Build/Launch/Grow/Evolve) linking to new `/services/` phase pages, with a fixed whole-pill click target. +- Added `patterns/sections/services-service-clusters.php`: five cluster cards in an asymmetric 3+2 layout grouping all 14 services, each linking out to its individual service page. +- Added `styles/sections/cards/card-cluster.json`, a new shared card shell for the service-clusters section. + +### Fixed + +- Fixed the second service-clusters card row's `blockGap` being nested outside `spacing` instead of inside it, which silently fell back to WordPress's default gap instead of matching row 1. + +--- + ## [Unreleased] — Build Search template (LS-2594) ### Added diff --git a/assets/css/services-hero.css b/assets/css/services-hero.css new file mode 100644 index 0000000..3c509ec --- /dev/null +++ b/assets/css/services-hero.css @@ -0,0 +1 @@ +.ls-service-pill{position:relative;transition:background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:translateY(-1px);box-shadow:var(--wp--custom--shadow--elevation--100)}.ls-service-pill a::before{content:"";position:absolute;inset:0;z-index:var(--wp--custom--z-index--base)}.ls-service-pill a:focus-visible{outline:2px solid var(--wp--custom--color--focus--ring);outline-offset:2px;border-radius:var(--wp--preset--border-radius--100)}.ls-service-pill--discover:hover,.ls-service-pill--discover:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--create:hover,.ls-service-pill--create:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--build:hover,.ls-service-pill--build:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--launch:hover,.ls-service-pill--launch:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--grow:hover,.ls-service-pill--grow:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--evolve:hover,.ls-service-pill--evolve:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 55%, var(--wp--custom--color--border--card)) !important}@media(prefers-reduced-motion: reduce){.ls-service-pill{transition:none}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:none}} diff --git a/assets/css/services-linked-decisions.css b/assets/css/services-linked-decisions.css new file mode 100644 index 0000000..6700041 --- /dev/null +++ b/assets/css/services-linked-decisions.css @@ -0,0 +1 @@ +.ls-process-pill{position:relative;transition:border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-process-pill:hover,.ls-process-pill:focus-within{border-color:var(--wp--custom--color--border--field);transform:translateY(-1px);box-shadow:var(--wp--custom--shadow--elevation--100)}.ls-process-pill .ls-process-pill__label a::before{content:"";position:absolute;inset:0;z-index:var(--wp--custom--z-index--base)}.ls-process-pill .ls-process-pill__label a:focus-visible{outline:2px solid var(--wp--custom--color--focus--ring);outline-offset:2px;border-radius:var(--wp--preset--border-radius--100)}.ls-process-pill .ls-process-pill__badge{transition:background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-process-pill--discover:hover .ls-process-pill__badge,.ls-process-pill--discover:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 50%, transparent) !important}.ls-process-pill--create:hover .ls-process-pill__badge,.ls-process-pill--create:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 50%, transparent) !important}.ls-process-pill--build:hover .ls-process-pill__badge,.ls-process-pill--build:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 50%, transparent) !important}.ls-process-pill--launch:hover .ls-process-pill__badge,.ls-process-pill--launch:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 50%, transparent) !important}.ls-process-pill--grow:hover .ls-process-pill__badge,.ls-process-pill--grow:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 50%, transparent) !important}.ls-process-pill--evolve:hover .ls-process-pill__badge,.ls-process-pill--evolve:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 50%, transparent) !important}@media(prefers-reduced-motion: reduce){.ls-process-pill{transition:none}.ls-process-pill:hover,.ls-process-pill:focus-within{transform:none}.ls-process-pill__badge{transition:none}} diff --git a/assets/css/services-service-clusters.css b/assets/css/services-service-clusters.css new file mode 100644 index 0000000..671f69f --- /dev/null +++ b/assets/css/services-service-clusters.css @@ -0,0 +1 @@ +.ls-cluster-tag{transition:background-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard)}.ls-cluster-tag:hover,.ls-cluster-tag:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--link--accent) 10%, transparent);border-color:var(--wp--custom--color--link--accent)}.ls-cluster-tag:hover .wp-block-outermost-icon-block .icon-container,.ls-cluster-tag:focus-within .wp-block-outermost-icon-block .icon-container{color:var(--wp--custom--color--text--brand) !important}@media(prefers-reduced-motion: reduce){.ls-cluster-tag{transition:none}} diff --git a/functions.php b/functions.php index 847aacf..76c0786 100644 --- a/functions.php +++ b/functions.php @@ -70,6 +70,9 @@ function ls_theme_setup() { add_editor_style( 'assets/css/card-shells.css' ); add_editor_style( 'assets/css/cta-buttons.css' ); add_editor_style( 'assets/css/home-hero.css' ); + add_editor_style( 'assets/css/services-hero.css' ); + add_editor_style( 'assets/css/services-linked-decisions.css' ); + add_editor_style( 'assets/css/services-service-clusters.css' ); add_editor_style( 'assets/css/work-hero.css' ); add_editor_style( 'assets/css/work-single-hero.css' ); add_editor_style( 'assets/css/blog-hero.css' ); diff --git a/inc/animations.php b/inc/animations.php index ea89e39..23a76e7 100644 --- a/inc/animations.php +++ b/inc/animations.php @@ -55,11 +55,11 @@ function ls_theme_get_local_asset_version( $path ) { */ function ls_theme_get_bundle_render_markers() { return array( - 'taxonomy-filter' => array( 'blocks' => array( 'ls-plugin/taxonomy-filter' ) ), - 'work-project-card' => array( + 'taxonomy-filter' => array( 'blocks' => array( 'ls-plugin/taxonomy-filter' ) ), + 'work-project-card' => array( 'classes' => array( 'is-style-card-case-study', 'ls-tag-pills', 'ls-card-banner-tint', 'ls-platform-tag-brand', 'ls-platform-tag-woocommerce' ), ), - 'work-archive-sections' => array( + 'work-archive-sections' => array( 'classes' => array( 'ls-icon-well-accent', 'ls-icon-well-brand', @@ -70,20 +70,23 @@ function ls_theme_get_bundle_render_markers() { 'ls-card-divider-both', ), ), - 'home-hero' => array( 'classes' => array( 'ls-home-hero-section' ) ), - 'work-hero' => array( 'classes' => array( 'ls-work-hero' ) ), - 'work-single-hero' => array( 'classes' => array( 'ls-work-single-meta' ) ), - 'blog-hero' => array( 'classes' => array( 'ls-blog-hero' ) ), - 'blog-all-articles' => array( 'classes' => array( 'is-style-card-post', 'ls-post-card-cta' ) ), - 'blog-writing-cta' => array( 'classes' => array( 'ls-writing-cta', 'ls-code-panel' ) ), - 'button-secondary' => array( 'classes' => array( 'is-style-button-secondary' ) ), - 'featured-work' => array( 'classes' => array( 'ls-featured-work-grid', 'ls-featured-work-card__divider' ) ), - 'where-to-fit' => array( 'classes' => array( 'ls-package-card' ) ), - 'homepage-cta' => array( 'classes' => array( 'ls-homepage-cta' ) ), - 'stats-bar' => array( 'classes' => array( 'ls-stats-row', 'ls-stat-item' ) ), - 'homepage-card-rows' => array( 'classes' => array( 'ls-homepage-card-row', 'ls-what-we-build-row' ) ), - 'search-hero' => array( 'classes' => array( 'ls-search-hero', 'ls-optical-trim' ) ), - 'search-results' => array( 'classes' => array( 'ls-search-result' ) ), + 'home-hero' => array( 'classes' => array( 'ls-home-hero-section' ) ), + 'services-hero' => array( 'classes' => array( 'ls-service-pill' ) ), + 'services-linked-decisions' => array( 'classes' => array( 'ls-process-pill' ) ), + 'services-service-clusters' => array( 'classes' => array( 'ls-cluster-tag' ) ), + 'work-hero' => array( 'classes' => array( 'ls-work-hero' ) ), + 'work-single-hero' => array( 'classes' => array( 'ls-work-single-meta' ) ), + 'blog-hero' => array( 'classes' => array( 'ls-blog-hero' ) ), + 'blog-all-articles' => array( 'classes' => array( 'is-style-card-post', 'ls-post-card-cta' ) ), + 'blog-writing-cta' => array( 'classes' => array( 'ls-writing-cta', 'ls-code-panel' ) ), + 'button-secondary' => array( 'classes' => array( 'is-style-button-secondary' ) ), + 'featured-work' => array( 'classes' => array( 'ls-featured-work-grid', 'ls-featured-work-card__divider' ) ), + 'where-to-fit' => array( 'classes' => array( 'ls-package-card' ) ), + 'homepage-cta' => array( 'classes' => array( 'ls-homepage-cta' ) ), + 'stats-bar' => array( 'classes' => array( 'ls-stats-row', 'ls-stat-item' ) ), + 'homepage-card-rows' => array( 'classes' => array( 'ls-homepage-card-row', 'ls-what-we-build-row' ) ), + 'search-hero' => array( 'classes' => array( 'ls-search-hero', 'ls-optical-trim' ) ), + 'search-results' => array( 'classes' => array( 'ls-search-result' ) ), ); } @@ -201,7 +204,7 @@ function ls_theme_print_late_effect_styles() { function ls_theme_get_effect_styles( $context = 'front' ) { $effects = array( // Genuinely global, sitewide styling — always loads, no condition. - 'effects' => array( + 'effects' => array( 'handle' => 'ls-theme-effects', 'path' => 'assets/css/animations.css', 'contexts' => array( 'front', 'editor' ), @@ -212,7 +215,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { // work-archive-sections' icon-well classes are also used by three homepage sections). // Conditions only apply in the 'front' context; the editor keeps loading every bundle // unconditionally so any pattern still looks correct when edited in isolation. - 'taxonomy-filter' => array( + 'taxonomy-filter' => array( 'handle' => 'ls-theme-taxonomy-filter', 'path' => 'assets/css/taxonomy-filter.css', 'contexts' => array( 'front', 'editor' ), @@ -221,7 +224,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_post_type_archive( 'project' ) || is_page_template( 'page-blog-archive' ); }, ), - 'work-project-card' => array( + 'work-project-card' => array( 'handle' => 'ls-theme-work-project-card', 'path' => 'assets/css/work-project-card.css', 'contexts' => array( 'front', 'editor' ), @@ -231,7 +234,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_front_page() || is_post_type_archive( 'project' ); }, ), - 'work-archive-sections' => array( + 'work-archive-sections' => array( 'handle' => 'ls-theme-work-archive-sections', 'path' => 'assets/css/work-archive-sections.css', 'contexts' => array( 'front', 'editor' ), @@ -245,7 +248,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_front_page() || is_post_type_archive( 'project' ) || is_search(); }, ), - 'card-shells' => array( + 'card-shells' => array( 'handle' => 'ls-theme-card-shells', 'path' => 'assets/css/card-shells.css', 'contexts' => array( 'front', 'editor' ), @@ -255,19 +258,38 @@ function ls_theme_get_effect_styles( $context = 'front' ) { // unstyled on every single use, not just a rare off-template case — so at ~10 KB // compressed it's cheaper to load unconditionally than to risk that. ), - 'cta-buttons' => array( + 'cta-buttons' => array( 'handle' => 'ls-theme-cta-buttons', 'path' => 'assets/css/cta-buttons.css', 'contexts' => array( 'front', 'editor' ), // Same reasoning as card-shells above (~5 KB compressed). ), - 'home-hero' => array( + 'home-hero' => array( 'handle' => 'ls-theme-home-hero', 'path' => 'assets/css/home-hero.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'work-hero' => array( + 'services-hero' => array( + 'handle' => 'ls-theme-services-hero', + 'path' => 'assets/css/services-hero.css', + 'contexts' => array( 'front', 'editor' ), + // No page template yet (LS-1598 in progress) — same reasoning as card-shells/ + // cta-buttons above, relies on render_block marker detection instead of a condition. + ), + 'services-linked-decisions' => array( + 'handle' => 'ls-theme-services-linked-decisions', + 'path' => 'assets/css/services-linked-decisions.css', + 'contexts' => array( 'front', 'editor' ), + // Same reasoning as services-hero above. + ), + 'services-service-clusters' => array( + 'handle' => 'ls-theme-services-service-clusters', + 'path' => 'assets/css/services-service-clusters.css', + 'contexts' => array( 'front', 'editor' ), + // Same reasoning as services-hero above. + ), + 'work-hero' => array( 'handle' => 'ls-theme-work-hero', 'path' => 'assets/css/work-hero.css', 'contexts' => array( 'front', 'editor' ), @@ -275,7 +297,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_post_type_archive( 'project' ); }, ), - 'work-single-hero' => array( + 'work-single-hero' => array( 'handle' => 'ls-theme-work-single-hero', 'path' => 'assets/css/work-single-hero.css', 'contexts' => array( 'front', 'editor' ), @@ -283,7 +305,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_singular( 'project' ); }, ), - 'blog-hero' => array( + 'blog-hero' => array( 'handle' => 'ls-theme-blog-hero', 'path' => 'assets/css/blog-hero.css', 'contexts' => array( 'front', 'editor' ), @@ -291,7 +313,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_page_template( 'page-blog-archive' ); }, ), - 'blog-all-articles' => array( + 'blog-all-articles' => array( 'handle' => 'ls-theme-blog-all-articles', 'path' => 'assets/css/blog-all-articles.css', 'contexts' => array( 'front', 'editor' ), @@ -299,7 +321,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_page_template( 'page-blog-archive' ); }, ), - 'blog-writing-cta' => array( + 'blog-writing-cta' => array( 'handle' => 'ls-theme-blog-writing-cta', 'path' => 'assets/css/blog-writing-cta.css', 'contexts' => array( 'front', 'editor' ), @@ -307,7 +329,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_page_template( 'page-blog-archive' ); }, ), - 'faq' => array( + 'faq' => array( 'handle' => 'ls-theme-faq', 'path' => 'assets/css/faq.css', 'contexts' => array( 'front', 'editor' ), @@ -322,12 +344,12 @@ function ls_theme_get_effect_styles( $context = 'front' ) { // of animations.css rather than kept there under the header/footer exception. The header // and footer template parts render on every page anyway, so a presence condition would // never actually skip loading this bundle — left unconditional. - 'links' => array( + 'links' => array( 'handle' => 'ls-theme-links', 'path' => 'assets/css/links.css', 'contexts' => array( 'front', 'editor' ), ), - 'button-secondary' => array( + 'button-secondary' => array( 'handle' => 'ls-theme-button-secondary', 'path' => 'assets/css/button-secondary.css', 'contexts' => array( 'front', 'editor' ), @@ -341,43 +363,43 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_front_page() || is_post_type_archive( 'project' ) || is_404(); }, ), - 'featured-work' => array( + 'featured-work' => array( 'handle' => 'ls-theme-featured-work', 'path' => 'assets/css/featured-work.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'where-to-fit' => array( + 'where-to-fit' => array( 'handle' => 'ls-theme-where-to-fit', 'path' => 'assets/css/where-to-fit.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'homepage-cta' => array( + 'homepage-cta' => array( 'handle' => 'ls-theme-homepage-cta', 'path' => 'assets/css/homepage-cta.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'stats-bar' => array( + 'stats-bar' => array( 'handle' => 'ls-theme-stats-bar', 'path' => 'assets/css/stats-bar.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'homepage-card-rows' => array( + 'homepage-card-rows' => array( 'handle' => 'ls-theme-homepage-card-rows', 'path' => 'assets/css/homepage-card-rows.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'homepage-why-lightspeed' => array( + 'homepage-why-lightspeed' => array( 'handle' => 'ls-theme-homepage-why-lightspeed', 'path' => 'assets/css/homepage-why-lightspeed.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'search-results' => array( + 'search-results' => array( 'handle' => 'ls-theme-search-results', 'path' => 'assets/css/search-results.css', 'contexts' => array( 'front', 'editor' ), diff --git a/package.json b/package.json index 3e59028..b888b5a 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,15 @@ "description": "LightSpeed Theme is a custom WordPress block theme built by LightSpeed for fast, accessible, maintainable websites using the WordPress Site Editor and block editor.", "type": "module", "scripts": { - "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/search-hero.scss:assets/css/search-hero.css", - "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/search-hero.scss:assets/css/search-hero.css", + "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/search-hero.scss:assets/css/search-hero.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css src/scss/structural/services-service-clusters.scss:assets/css/services-service-clusters.css", + "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/search-hero.scss:assets/css/search-hero.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css src/scss/structural/services-service-clusters.scss:assets/css/services-service-clusters.css", "schema:validate": "node theme-utils.mjs validate-schema", "theme:validate": "node theme-utils.mjs validate-theme", "patterns:escape": "node theme-utils.mjs escape-patterns", "security:scan": "node theme-utils.mjs security-scan", "lint": "npm run lint:json", "lint:json": "node --input-type=module --eval \"import { readFileSync } from 'fs'; import { glob } from 'glob'; const files = await glob(['theme.json', 'styles/**/*.json']); let ok = true; for (const f of files) { try { JSON.parse(readFileSync(f, 'utf8')); } catch (e) { console.error('Invalid JSON:', f, e.message); ok = false; } } if (ok) console.log('All JSON files are valid.'); else process.exit(1);\"", - "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/search-hero.scss:assets/css/search-hero.css" + "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/search-hero.scss:assets/css/search-hero.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css src/scss/structural/services-service-clusters.scss:assets/css/services-service-clusters.css" }, "devDependencies": { "@axe-core/playwright": "^4.13.0", diff --git a/patterns/hero/services-hero.php b/patterns/hero/services-hero.php new file mode 100644 index 0000000..84771a1 --- /dev/null +++ b/patterns/hero/services-hero.php @@ -0,0 +1,225 @@ + __( 'Discovery', 'ls-theme' ), + 'url' => '/services/discovery/', + 'phase' => 'discover', + 'icon' => 'search', + ), + array( + 'label' => __( 'Content', 'ls-theme' ), + 'url' => '/services/content/', + 'phase' => 'create', + 'icon' => 'file-text', + ), + array( + 'label' => __( 'Design', 'ls-theme' ), + 'url' => '/services/design/', + 'phase' => 'create', + 'icon' => 'paint-brush', + ), + array( + 'label' => __( 'Development', 'ls-theme' ), + 'url' => '/services/development/', + 'phase' => 'build', + 'icon' => 'code', + ), + array( + 'label' => __( 'Migrations', 'ls-theme' ), + 'url' => '/services/migrations/', + 'phase' => 'build', + 'icon' => 'arrows-left-right', + ), + array( + 'label' => __( 'Hosting', 'ls-theme' ), + 'url' => '/services/hosting/', + 'phase' => 'launch', + 'icon' => 'cloud', + ), + array( + 'label' => __( 'Performance', 'ls-theme' ), + 'url' => '/services/performance/', + 'phase' => 'launch', + 'icon' => 'gauge', + ), + array( + 'label' => __( 'Security', 'ls-theme' ), + 'url' => '/services/security/', + 'phase' => 'launch', + 'icon' => 'shield', + ), + array( + 'label' => __( 'Training', 'ls-theme' ), + 'url' => '/services/training/', + 'phase' => 'launch', + 'icon' => 'graduation-cap', + ), + array( + 'label' => __( 'Support', 'ls-theme' ), + 'url' => '/services/support/', + 'phase' => 'grow', + 'icon' => 'lifebuoy', + ), + array( + 'label' => __( 'SEO', 'ls-theme' ), + 'url' => '/services/seo/', + 'phase' => 'grow', + 'icon' => 'chart-line-up', + ), + array( + 'label' => __( 'Accessibility', 'ls-theme' ), + 'url' => '/services/accessibility/', + 'phase' => 'grow', + 'icon' => 'wheelchair', + ), + array( + 'label' => __( 'Email marketing', 'ls-theme' ), + 'url' => '/services/email-marketing/', + 'phase' => 'grow', + 'icon' => 'envelope', + ), + array( + 'label' => __( 'AI', 'ls-theme' ), + 'url' => '/services/ai/', + 'phase' => 'evolve', + 'icon' => 'special-interests', + ), +); +?> + +
+ + +
+ + + + + + +
+ + + +

+ +
+ + + +
+ + +
+ + +

+ + + +
+ +

+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + +
+ + + +

+ +
+ + +
+ +
+ + + +
+ +
+ + +

+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ +

+ + + +

+ +
+ +
+ +
+ +
+ +
+ +
+ diff --git a/patterns/sections/services-linked-decisions.php b/patterns/sections/services-linked-decisions.php new file mode 100644 index 0000000..97833a0 --- /dev/null +++ b/patterns/sections/services-linked-decisions.php @@ -0,0 +1,129 @@ + '01', + 'label' => __( 'Discover', 'ls-theme' ), + 'phase' => 'discover', + 'url' => '/services/discover/', + ), + array( + 'index' => '02', + 'label' => __( 'Create', 'ls-theme' ), + 'phase' => 'create', + 'url' => '/services/create/', + ), + array( + 'index' => '03', + 'label' => __( 'Build', 'ls-theme' ), + 'phase' => 'build', + 'url' => '/services/build/', + ), + array( + 'index' => '04', + 'label' => __( 'Launch', 'ls-theme' ), + 'phase' => 'launch', + 'url' => '/services/launch/', + ), + array( + 'index' => '05', + 'label' => __( 'Grow', 'ls-theme' ), + 'phase' => 'grow', + 'url' => '/services/grow/', + ), + array( + 'index' => '06', + 'label' => __( 'Evolve', 'ls-theme' ), + 'phase' => 'evolve', + 'url' => '/services/evolve/', + ), +); +?> + +
+ + +
+ + +
+ + +
+ + +
+ + + +

+ +
+ + + +

+ +
+ + + +
+ + +
+ +

+ +
+ + + +
+ $ls_step ) : + $ls_step_phase_color = 'var(--wp--custom--color--phase--' . $ls_step['phase'] . ')'; + $ls_badge_bg = 'color-mix(in srgb, ' . $ls_step_phase_color . ' 10%, transparent)'; + $ls_badge_border = 'color-mix(in srgb, ' . $ls_step_phase_color . ' 30%, transparent)'; + $ls_pill_class = 'ls-process-pill ls-process-pill--' . $ls_step['phase']; + ?> + + +
+ +

+ + + +

+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ diff --git a/patterns/sections/services-service-clusters.php b/patterns/sections/services-service-clusters.php new file mode 100644 index 0000000..2f1257d --- /dev/null +++ b/patterns/sections/services-service-clusters.php @@ -0,0 +1,238 @@ + 'search', + 'content' => 'file-text', + 'design' => 'paint-brush', + 'development' => 'code', + 'migrations' => 'arrows-left-right', + 'hosting' => 'cloud', + 'training' => 'graduation-cap', + 'support' => 'lifebuoy', + 'ai' => 'sparkle', + 'seo' => 'chart-line-up', +); + +$ls_service_urls = array( + 'discovery' => '/services/discovery/', + 'content' => '/services/content/', + 'design' => '/services/design/', + 'development' => '/services/development/', + 'migrations' => '/services/migrations/', + 'hosting' => '/services/hosting/', + 'training' => '/services/training/', + 'support' => '/services/support/', + 'ai' => '/services/ai/', + 'seo' => '/services/seo/', +); + +$ls_service_labels = array( + 'discovery' => __( 'Discovery', 'ls-theme' ), + 'content' => __( 'Content', 'ls-theme' ), + 'design' => __( 'Design', 'ls-theme' ), + 'development' => __( 'Development', 'ls-theme' ), + 'migrations' => __( 'Migrations', 'ls-theme' ), + 'hosting' => __( 'Hosting', 'ls-theme' ), + 'training' => __( 'Training', 'ls-theme' ), + 'support' => __( 'Support', 'ls-theme' ), + 'ai' => __( 'AI', 'ls-theme' ), + 'seo' => __( 'SEO', 'ls-theme' ), +); + +$ls_clusters = array( + array( + 'index' => '01', + 'icon' => 'search', + 'title' => __( 'Discovery and content', 'ls-theme' ), + 'description' => __( 'For scope clarity, content structure, risk reduction and better planning before design or build decisions harden.', 'ls-theme' ), + 'tags' => array( 'discovery', 'content' ), + ), + array( + 'index' => '02', + 'icon' => 'paint-brush', + 'title' => __( 'Design systems and accessibility', 'ls-theme' ), + 'description' => __( 'For clearer interfaces, stronger consistency and more maintainable design-to-development translation.', 'ls-theme' ), + 'tags' => array( 'design' ), + ), + array( + 'index' => '03', + 'icon' => 'code', + 'title' => __( 'Engineering and migrations', 'ls-theme' ), + 'description' => __( 'For custom WordPress work, integrations, migrations and cleaner long-term architecture.', 'ls-theme' ), + 'tags' => array( 'development', 'migrations' ), + ), + array( + 'index' => '04', + 'icon' => 'rocket', + 'title' => __( 'Launch and infrastructure', 'ls-theme' ), + 'description' => __( 'For hosting, readiness, security-minded thinking and performance-aware go-live planning.', 'ls-theme' ), + 'tags' => array( 'hosting', 'training' ), + ), + array( + 'index' => '05', + 'icon' => 'question', + 'title' => __( 'Support and optimisation', 'ls-theme' ), + 'description' => __( 'For maintenance, issue resolution, performance review, training and long-term platform continuity.', 'ls-theme' ), + 'tags' => array( 'support', 'ai', 'seo' ), + ), +); + +/** + * Renders one service-cluster card. A local closure, not a named function — this file can be + * included more than once per request (pattern registration + a live "wp:pattern" reference), + * and a top-level `function` declaration here would fatal with "cannot redeclare" on the second + * include. + * + * @param array $ls_cluster Cluster data (index, icon, title, description, tags). + */ +$ls_render_cluster = function ( $ls_cluster ) use ( $ls_service_icons, $ls_service_urls, $ls_service_labels ) { + ?> + +
+ + +
+ +
+ +
+ + + +

+ +
+ + + +
+ +

+ + + +

+ +
+ + + +
+ + + +
+ + + +

+ + + +
+ + + +
+ +
+ + + +
+ + +
+ + +
+ + +
+ +
+ + + +

+ +
+ + + +

+ +
+ + + +
+ +
+ +

+ +
+ +
+ +
+ + + +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+ + + +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ +
+ diff --git a/src/scss/structural/services-hero.scss b/src/scss/structural/services-hero.scss new file mode 100644 index 0000000..24647ca --- /dev/null +++ b/src/scss/structural/services-hero.scss @@ -0,0 +1,80 @@ +/********** Services Hero (Services page only) **********/ + +/* + * Services Hero (LS-1598, single consumer patterns/hero/services-hero.php). The service-tag + * pills' rest-state background/border tints are set inline per instance (each pill mixes its + * own phase token via colour-mix()) because theme.json has no key for six distinct + * phase-derived tints — that part is not this file's concern. What lives here is only the + * states theme.json genuinely cannot express: see the JSON limitation comments below. + */ +.ls-service-pill { + // JSON limitation: position:relative here so the whole-pill ::before overlay below has a + // containing block to size against — see AGENTS.md Theme-First Approach ("width", "max-width" + // and similar structural properties JSON has no key for). + position: relative; + + // JSON limitation: block-level :hover/:focus-within has no theme.json pseudo-state key for + // an arbitrary custom class (theme.json's elements.*.:hover only covers link/button/heading/ + // caption/cite sitewide, not a scoped custom component) — see AGENTS.md Theme-First Approach. + transition: + background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard); + + &:hover, + &:focus-within { + transform: translateY(-1px); + box-shadow: var(--wp--custom--shadow--elevation--100); + } + + // JSON limitation: content:"" pseudo-elements have no theme.json equivalent. Only the label + // text is a real , but the whole pill should be clickable, so this ::before is stretched + // to the pill's full box (same technique already used by "Card - Services", see + // styles/sections/cards/card-services.json's .ls-card-services__cta ::before). The anchor + // itself must stay position:static (no position/z-index of its own) so this ::before's + // containing block resolves up to .ls-service-pill instead of shrinking to the anchor's own + // small text box. + a::before { + content: ""; + position: absolute; + inset: 0; + z-index: var(--wp--custom--z-index--base); + } + + // JSON limitation: :focus-visible has no theme.json pseudo-state key — see AGENTS.md + // Theme-First Approach. + a:focus-visible { + outline: 2px solid var(--wp--custom--color--focus--ring); + outline-offset: 2px; + border-radius: var(--wp--preset--border-radius--100); + } +} + +@each $phase in discover, create, build, launch, grow, evolve { + .ls-service-pill--#{$phase} { + // JSON limitation: same :hover/:focus-within gap as above. The `!important` here is also + // required, separately: background-color/border-color are set inline per instance at rest + // (colour-mix over each pill's own phase token, itself a JSON limitation — six distinct + // phase-derived tints have no theme.json key), and an inline style attribute always + // outranks an external stylesheet rule regardless of pseudo-class or specificity, so + // without !important these two properties could never change on hover. transform/ + // box-shadow aren't set inline at rest, so they don't need it. + &:hover, + &:focus-within { + background-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 16%, transparent) !important; + border-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 55%, var(--wp--custom--color--border--card)) !important; + } + } +} + +@media (prefers-reduced-motion: reduce) { + .ls-service-pill { + transition: none; + + &:hover, + &:focus-within { + transform: none; + } + } +} diff --git a/src/scss/structural/services-linked-decisions.scss b/src/scss/structural/services-linked-decisions.scss new file mode 100644 index 0000000..5ae2bb4 --- /dev/null +++ b/src/scss/structural/services-linked-decisions.scss @@ -0,0 +1,91 @@ +/********** Services - Linked Decisions (Services page only) **********/ + +/* + * Services Linked Decisions (LS-1598, single consumer + * patterns/sections/services-linked-decisions.php). The process pills stay neutral overall + * (white/transparent background, dark label) with only the number badge carrying the phase + * colour, so the row doesn't compete with the hero's own phase-tinted tag row above it. See + * the JSON limitation comments below for what specifically requires CSS here. + */ +.ls-process-pill { + position: relative; + + // JSON limitation: block-level :hover/:focus-within has no theme.json pseudo-state key for + // an arbitrary custom class (theme.json's elements.*.:hover only covers link/button/heading/ + // caption/cite sitewide, not a scoped custom component) — see AGENTS.md Theme-First Approach. + transition: + border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard); + + &:hover, + &:focus-within { + border-color: var(--wp--custom--color--border--field); + transform: translateY(-1px); + box-shadow: var(--wp--custom--shadow--elevation--100); + } + + .ls-process-pill__label a { + // JSON limitation: content:"" pseudo-elements have no theme.json equivalent — see AGENTS.md + // Theme-First Approach. Only the label text is a real , but the whole pill should be + // clickable, so this ::before is stretched to the pill's full box (same technique already + // used by "Card - Services", see styles/sections/cards/card-services.json's + // .ls-card-services__cta ::before). The anchor itself must stay position:static (no + // position/z-index of its own) so this ::before's containing block resolves up to + // .ls-process-pill (which has position:relative) instead of shrinking to the anchor's own + // small text box — that was the original bug here. + &::before { + content: ""; + position: absolute; + inset: 0; + z-index: var(--wp--custom--z-index--base); + } + + // JSON limitation: :focus-visible has no theme.json pseudo-state key — see AGENTS.md + // Theme-First Approach. + &:focus-visible { + outline: 2px solid var(--wp--custom--color--focus--ring); + outline-offset: 2px; + border-radius: var(--wp--preset--border-radius--100); + } + } + + .ls-process-pill__badge { + transition: + background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard); + } +} + +@each $phase in discover, create, build, launch, grow, evolve { + .ls-process-pill--#{$phase} { + // JSON limitation: same :hover/:focus-within gap as above. The `!important` here is also + // required, separately: the badge's background-color/border-color are set inline per + // instance at rest (colour-mix over each pill's own phase token, itself a JSON + // limitation — six distinct phase-derived tints have no theme.json key), and an inline + // style attribute always outranks an external stylesheet rule regardless of pseudo-class, + // so without !important these two properties could never change on hover. + &:hover, + &:focus-within { + .ls-process-pill__badge { + background-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 20%, transparent) !important; + border-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 50%, transparent) !important; + } + } + } +} + +@media (prefers-reduced-motion: reduce) { + .ls-process-pill { + transition: none; + + &:hover, + &:focus-within { + transform: none; + } + } + + .ls-process-pill__badge { + transition: none; + } +} diff --git a/src/scss/structural/services-service-clusters.scss b/src/scss/structural/services-service-clusters.scss new file mode 100644 index 0000000..ddc8260 --- /dev/null +++ b/src/scss/structural/services-service-clusters.scss @@ -0,0 +1,36 @@ +/********** Services - Service Clusters (Services page only) **********/ + +/* + * Services Service Clusters (LS-1598, single consumer + * patterns/sections/services-service-clusters.php). Rest-state look, the stretched-link + * ::before, and :focus-visible all live in styles/sections/cards/card-cluster.json — this file + * only carries the hover-state motion, matching the existing "Card - Link Row" convention (see + * .is-style-card-link-row in src/scss/structural/work-archive-sections.scss): a background/ + * border-color tint swap on hover, nothing heavier. The card itself has no hover-lift — unlike + * Card - Category, it isn't a single link, so it shouldn't look interactive as a whole. + */ +.ls-cluster-tag { + // JSON limitation: block-level :hover/:focus-within has no theme.json pseudo-state key for + // an arbitrary custom class (theme.json's elements.*.:hover only covers link/button/heading/ + // caption/cite sitewide, not a scoped custom component) — see AGENTS.md Theme-First Approach. + transition: + background-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard), + border-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard); + + &:hover, + &:focus-within { + background-color: color-mix(in srgb, var(--wp--custom--color--link--accent) 10%, transparent); + border-color: var(--wp--custom--color--link--accent); + } + + &:hover .wp-block-outermost-icon-block .icon-container, + &:focus-within .wp-block-outermost-icon-block .icon-container { + color: var(--wp--custom--color--text--brand) !important; + } +} + +@media (prefers-reduced-motion: reduce) { + .ls-cluster-tag { + transition: none; + } +} diff --git a/styles/sections/cards/card-cluster.json b/styles/sections/cards/card-cluster.json new file mode 100644 index 0000000..1e260b7 --- /dev/null +++ b/styles/sections/cards/card-cluster.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://schemas.wp.org/wp/6.9/theme.json", + "version": 3, + "title": "Card - Cluster", + "slug": "card-cluster", + "blockTypes": [ + "core/group" + ], + "description": "Service-cluster card shell: icon well + index number header, H3 heading, supporting copy, and a footer row of small clickable tag links (each its own stretched-link pill), separated by a top divider. Unlike Card - Category, the card itself is not a single link — only the footer tags are. Non-expressible structural properties (position, overflow, the nested content flex rule, each tag's own stretched-link containing block) stay in this css field; the tag hover-tint transition stays in src/scss/structural/services-service-clusters.scss (a bare &:hover in this css field is silently dropped, same class of bug as @media/@supports stripping).", + "styles": { + "color": { + "background": "var:custom|color|surface|canvas", + "text": "var:custom|color|text|default" + }, + "border": { + "color": "var:custom|color|border|card", + "radius": "var:preset|border-radius|300", + "style": "solid", + "width": "1px" + }, + "spacing": { + "blockGap": "var:preset|spacing|20", + "padding": { + "top": "var:preset|spacing|30", + "right": "var:preset|spacing|30", + "bottom": "var:preset|spacing|30", + "left": "var:preset|spacing|30" + } + }, + "css": "position: relative;\noverflow: hidden;\n\n& .ls-card-cluster__content {\n\tflex: 1 1 auto;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--wp--preset--spacing--10);\n}\n\n& .ls-card-cluster__tags {\n\tborder-top: 1px solid var(--wp--custom--color--border--card);\n}\n\n& .ls-cluster-tag {\n\tposition: relative;\n}\n\n& .ls-cluster-tag__link::before {\n\tcontent: \"\";\n\tposition: absolute;\n\tinset: 0;\n}\n\n& .ls-cluster-tag__link:focus-visible {\n\toutline: 2px solid var(--wp--custom--color--focus--ring);\n\toutline-offset: 2px;\n}" + } +}