srcdev-nuxt-components 9.5.7 → 9.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/.claude/skills/component-compliance-checklist.md +8 -2
  2. package/.claude/skills/component-dynamic-slots.md +3 -3
  3. package/.claude/skills/component-export-types.md +10 -16
  4. package/.claude/skills/component-naming.md +1 -1
  5. package/.claude/skills/components/alert-content.md +9 -1
  6. package/.claude/skills/components/breadcrumb.md +5 -0
  7. package/.claude/skills/components/container-glow.md +21 -8
  8. package/.claude/skills/components/display-avatar.md +13 -7
  9. package/.claude/skills/components/display-chip.md +37 -24
  10. package/.claude/skills/components/display-pill.md +58 -8
  11. package/.claude/skills/components/display-prompt.md +8 -5
  12. package/.claude/skills/components/display-toast.md +21 -4
  13. package/.claude/skills/components/glowing-border.md +20 -2
  14. package/.claude/skills/components/input-button.md +3 -0
  15. package/.claude/skills/components/responsive-header.md +59 -0
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/service-detail.md +32 -0
  18. package/.claude/skills/components/service-summary.md +8 -0
  19. package/.claude/skills/components/services-card-grid.md +5 -3
  20. package/.claude/skills/components/services-card.md +22 -12
  21. package/.claude/skills/components/site-header.md +2 -2
  22. package/.claude/skills/components/slider-gallery.md +10 -4
  23. package/.claude/skills/components/tabbed-content.md +300 -0
  24. package/.claude/skills/composable-anchor-scroll.md +16 -15
  25. package/.claude/skills/index.md +4 -5
  26. package/.claude/skills/storybook-add-story.md +53 -0
  27. package/.claude/skills/theming-status-tokens.md +60 -0
  28. package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
  29. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  30. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  31. package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
  32. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  33. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  34. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  35. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  36. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  37. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  38. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  39. package/app/assets/styles/setup/03.theming/index.css +1 -0
  40. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  41. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  42. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  43. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  44. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  45. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  46. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  47. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  48. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  49. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  50. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
  51. package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
  52. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  53. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  54. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  55. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  56. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  57. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  58. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  59. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  60. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  61. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  62. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  63. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  64. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  65. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  66. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  67. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  68. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
  69. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
  70. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
  71. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
  72. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
  73. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
  74. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  75. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  76. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  77. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  78. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  79. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  80. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  81. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  82. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  83. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  84. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  85. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  86. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  87. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  88. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  89. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  90. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  91. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  92. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  93. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  94. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  95. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  96. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  97. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  98. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  99. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
  102. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
  103. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
  104. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
  105. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  106. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  107. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  108. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  109. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
  110. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  111. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  112. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  113. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  114. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
  115. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  116. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  117. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  118. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  119. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  120. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  121. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  122. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  123. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  124. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  125. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
  126. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  127. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  128. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  129. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  130. package/app/composables/useAnchorScroll.ts +1 -1
  131. package/app/composables/useTabs.ts +148 -206
  132. package/app/types/components/display-chip.d.ts +8 -4
  133. package/app/types/components/display-toast.d.ts +2 -0
  134. package/app/types/components/index.ts +0 -1
  135. package/app/types/components/select-menu.d.ts +2 -0
  136. package/app/utils/pausableTimeout.ts +41 -0
  137. package/nuxt.config.ts +1 -1
  138. package/package.json +1 -1
  139. package/.claude/skills/components/site-navigation.md +0 -145
  140. package/.claude/skills/components/tab-navigation.md +0 -272
  141. package/.claude/skills/components/tabs-core.md +0 -187
  142. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  143. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  144. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  145. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  146. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  147. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  148. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  149. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  150. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  151. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  152. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
  153. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
  154. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
  155. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
  156. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
  157. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  158. package/app/composables/useNavCollapse.ts +0 -86
  159. package/app/types/components/nav-item.d.ts +0 -11
@@ -1,40 +1,8 @@
1
+ # ServiceDetail — Consumer Styling Guide
1
2
 
2
3
  > **Changed 2026-09-27**: `--service-detail-hero-pill-bg` (and the hero text colour on the pills)
3
4
  > never applied before, because `DisplayPill`'s `neutral` variant ignored the base tokens. It now
4
5
  > falls back to them, so the hero pills are transparent with the hero text colour as documented.
5
- # ServiceDetail — Consumer Styling Guide
6
-
7
- ## Layout building blocks
8
-
9
- The hero banner is a [GridStack](../../../../../.claude/skills/components/grid-stack.md) with two
10
- stacked layers — an `image` layer (the service photo) behind a `content` layer (the scrim +
11
- breadcrumb/title/pills). Each layer's root element is itself a
12
- [PageRow](../../../../../.claude/skills/components/page-row.md): `.service-detail__hero-image-row`
13
- (prop `heroImageVariant`, default `"full"`) wraps the photo, and `.service-detail__hero-overlay`
14
- (prop `heroContentVariant`, default `"content"`) *is* the scrim/alignment element, not a wrapper
15
- around one — a `PageRow`'s own box always spans the full width of its parent regardless of variant,
16
- only its children default into the variant's grid column, so the one element can carry the
17
- full-bleed scrim while its child (`.service-detail__hero-content`) still gets pulled to the
18
- configured column. This means the photo bleeds to whatever width `.service-detail` itself is allowed
19
- (edge-to-edge if `ServiceDetail` is placed without a content-constraining wrapper around it), while
20
- the breadcrumb/title/pills always stay pulled into the page's normal content-width column,
21
- regardless of how wide the photo bleeds. `--service-detail-hero-padding` now only controls
22
- **block** (vertical) padding on the scrim — horizontal alignment of the text is handled entirely by
23
- `.service-detail__hero-overlay`'s own `PageRow` grid, not by padding, so it can't fight the page's
24
- content-grid gutters.
25
-
26
- The two-column body (main content + sticky sidebar) is wrapped in its own `PageRow`, whose variant
27
- is a prop: `bodyVariant` (`"full" | "popout" | "content" | "inset-content"`, default `"content"`).
28
- The closing full-width CTA banner is wrapped the same way via `finalCtaVariant` (same type, same
29
- `"content"` default). This means a consumer no longer needs to wrap `ServiceDetail` itself in an
30
- outer `PageRow` purely to constrain either section's width — pass `body-variant`/`final-cta-variant`
31
- directly. It's still fine (and necessary for a genuine full-bleed hero) to place `ServiceDetail`
32
- inside an outer, wider `PageRow` — see
33
- [service-summary.md](../../../../../.claude/skills/components/service-summary.md)-style nesting
34
- notes in [page-row.md](../../../../../.claude/skills/components/page-row.md) for how nested page-rows
35
- compose.
36
-
37
- ---
38
6
 
39
7
  ## Public token API
40
8
 
@@ -44,12 +12,25 @@ compose.
44
12
  |---|---|---|
45
13
  | `--service-detail-hero-border-radius` | `0.8rem` | Corner rounding on the hero (clips the image/content layers — set to `0` for a true edge-to-edge bleed hero) |
46
14
  | `--service-detail-hero-content-align` | `end` | `align-content` on `.service-detail__hero-content` (the breadcrumb/title/pills wrapper) — where that content sits within the hero's block axis (`end` = bottom-anchored) |
47
- | `--service-detail-hero-min-height-mobile` | `32rem` | Hero banner height below the `768px` container breakpoint. The hero image always fills this height (`object-fit`/`object-position` control cropping) |
48
- | `--service-detail-hero-min-height-tablet` | `36rem` | Hero banner height from the `768px` container breakpoint |
49
- | `--service-detail-hero-min-height-desktop` | `42rem` | Hero banner height from the `1024px` container breakpoint |
15
+ | `--service-detail-hero-min-height-mobile` | `32rem` | Minimum hero banner height below the `768px` container breakpoint. The hero grows past it when its content (a long title, say) needs more room; the image always fills the hero (`object-fit`/`object-position` control cropping) |
16
+ | `--service-detail-hero-min-height-tablet` | `36rem` | Minimum hero banner height from the `768px` container breakpoint |
17
+ | `--service-detail-hero-min-height-desktop` | `42rem` | Minimum hero banner height from the `1024px` container breakpoint |
18
+
19
+ > Changed 2026-10-06: these were applied as a fixed `height`, so a long title was clipped by the
20
+ > hero. They are now a true minimum (`min-block-size`), matching their names.
21
+ >
22
+ > Also 2026-10-06: line-clamp tokens for the hero eyebrow and title, sidebar labels and values,
23
+ > and the final CTA heading and body. The hero title's and final CTA heading's spacing moved from
24
+ > utility classes (`mb-20`, `mbs-0`/`mbe-8`) into the component CSS, same values, so the clamp can
25
+ > keep descenders and italic overhang visible.
50
26
  | `--service-detail-hero-padding` | `3.2rem` | **Block-axis (vertical)** padding around the breadcrumb/title/pills content — horizontal alignment comes from the nested content `PageRow`, not this token |
51
27
  | `--service-detail-hero-scrim` | `linear-gradient(0deg, rgb(0 0 0 / 70%) 0%, rgb(0 0 0 / 10%) 60%, transparent 100%)` | Overlay gradient behind the hero text, for legibility over the image |
52
28
  | `--service-detail-hero-text-colour` | `white` | Colour of the breadcrumb, eyebrow, and title over the hero image |
29
+ | `--service-detail-hero-eyebrow-line-clamp` | `none` | Max lines of the hero eyebrow (subtitle), ellipsis on the last. `1` is single-line ellipsis |
30
+ | `--service-detail-hero-title-line-clamp` | `none` | Max lines of the hero title |
31
+
32
+ For the breadcrumb, set Breadcrumb's own `--breadcrumb-item-line-clamp` (it reaches the hero
33
+ breadcrumb from any ancestor).
53
34
  | `--service-detail-breadcrumb-margin-block-end` | `0.8rem` | Space below the breadcrumb |
54
35
  | `--service-detail-hero-pills-gap` | `0.8rem` | Gap between the duration and price pills |
55
36
  | `--service-detail-hero-pill-bg` | `transparent` | Background of the price/duration pills (via `DisplayPill`'s `--display-pill-background`) |
@@ -100,8 +81,8 @@ target `--stepper-list-*` directly under `.service-detail__process` — see
100
81
  | `--service-detail-ideal-for-gap` | `1.2rem` | Gap between ideal-for cards |
101
82
  | `--service-detail-ideal-for-item-gap` | `1rem` | Gap between icon and text within a card |
102
83
  | `--service-detail-ideal-for-item-padding` | `1.6rem` | Padding inside a card |
103
- | `--service-detail-ideal-for-item-background` | `var(--slate-09)` | Card background colour |
104
- | `--service-detail-ideal-for-item-border-colour` | `var(--slate-06)` | Card border colour |
84
+ | `--service-detail-ideal-for-item-background` | `var(--theme-surface-subtle)` | Card background colour |
85
+ | `--service-detail-ideal-for-item-border-colour` | `transparent` | Card border colour |
105
86
  | `--service-detail-ideal-for-item-border-radius` | `0.4rem` | Card corner rounding |
106
87
  | `--service-detail-ideal-for-icon-size` | `1.6rem` | Icon size |
107
88
  | `--service-detail-ideal-for-icon-colour` | `var(--colour-text-accent)` | Icon colour |
@@ -129,17 +110,28 @@ The ideal-for grid becomes 2 columns at a **500px container width**.
129
110
  | `--service-detail-sidebar-row-book-cta-padding-block` | `2rem` | Vertical padding on the `book-cta` slot's own row (only renders when the slot has content) |
130
111
  | `--service-detail-sidebar-row-book-cta-justify-content` | `end` | Horizontal alignment of the `book-cta` slot content within its row |
131
112
  | `--service-detail-sidebar-row-label-font-size` | `1.2rem` | Font size of a row's label (e.g. "Price") |
113
+ | `--service-detail-sidebar-label-line-clamp` | `none` | Max lines of the "Book This Service" / "You May Also Like" labels |
114
+ | `--service-detail-sidebar-value-line-clamp` | `none` | Max lines of a row's value (price, duration, location) |
115
+
116
+ A row's label takes the width of its own words (up to half the row) and wraps between words only;
117
+ the value takes the rest and wraps anywhere.
132
118
  | `--service-detail-sidebar-note-font-size` | `1.2rem` | Font size of the `sidebar-note` slot content |
133
119
  | `--service-detail-sidebar-note-margin-block-start` | `1.2rem` | Space above the `sidebar-note` slot content |
134
120
  | `--service-detail-related-items-gap` | `1rem` | Gap between related-service item cards |
135
- | `--service-detail-related-item-background` | `var(--slate-09)` | Background colour of a related-service item's card |
121
+ | `--service-detail-related-item-background` | `var(--theme-surface-subtle)` | Background colour of a related-service item's card |
136
122
  | `--service-detail-related-item-padding` | `1rem` | Padding inside a related-service item's card |
137
- | `--service-detail-related-item-border-colour` | `var(--slate-06)` | Border colour of a related-service item's card |
123
+ | `--service-detail-related-item-border-colour` | `transparent` | Border colour of a related-service item's card |
138
124
  | `--service-detail-related-item-border-radius` | `0.4rem` | Corner rounding of a related-service item's card |
139
125
  | `--service-detail-related-item-gap` | `1.2rem` | Gap between a related item's thumbnail and its text |
140
126
  | `--service-detail-related-image-size` | `5.6rem` | Width/height of a related-service thumbnail |
141
127
  | `--service-detail-related-image-border-radius` | `0.4rem` | Corner rounding of a related-service thumbnail |
142
128
  | `--service-detail-related-price-font-size` | `1.2rem` | Font size of a related item's price |
129
+ | `--service-detail-related-title-line-clamp` | `none` | Max lines of a related item's title (default item only, not the `related-service` slot). `1` is single-line ellipsis |
130
+
131
+ > Changed 2026-10-06: the ideal-for and related-service cards defaulted to dark `--slate-09` with a
132
+ > `--slate-06` border but set no text colour, so on a light page their inherited dark text was
133
+ > unreadable. They now default to `var(--theme-surface-subtle)` with a transparent border. To keep
134
+ > dark cards, set the four `-background`/`-border-colour` tokens.
143
135
  | `--service-detail-related-price-margin-block-start` | `0.6rem` | Space between a related item's title and its price |
144
136
  | `--service-detail-muted-opacity` | `0.7` | Opacity shared by every secondary/muted text element (sidebar label, sidebar note, related price, final CTA body) |
145
137
 
@@ -153,6 +145,11 @@ The booking card is a `GlassPanel` — its own `--glass-panel-*` tokens also app
153
145
  | `--service-detail-final-cta-margin-block-start` | `3.2rem` | Space above the banner |
154
146
  | `--service-detail-final-cta-padding-block` | `3.2rem 3.2rem` | Padding (block-start block-end) around the banner content, below its divider |
155
147
  | `--service-detail-final-cta-divider-colour` | `currentColor` | Divider line above the banner |
148
+ | `--service-detail-final-cta-heading-line-clamp` | `none` | Max lines of the banner heading |
149
+ | `--service-detail-final-cta-body-line-clamp` | `none` | Max lines of the banner body text |
150
+
151
+ The `final-cta` slot sits in `.service-detail__final-cta-action`, which never shrinks: long copy
152
+ wraps beside it instead of pushing the button out of the banner.
156
153
 
157
154
  The banner becomes a single row (copy left, CTA right) at a **700px container width**. Its own
158
155
  breakout width is controlled by the `finalCtaVariant` prop (default `"content"`), not a token —
@@ -160,6 +157,47 @@ see Layout building blocks above.
160
157
 
161
158
  ---
162
159
 
160
+ ## State hooks
161
+
162
+ No state classes. Inner classes are all `service-detail__*` (hero, hero image/overlay/content, breadcrumb,
163
+ hero pills, body, main, process, ideal-for and its items/icon, faqs and each faq/question, sidebar,
164
+ booking card, sidebar label/rows/note, related items, final CTA). FAQ questions render one heading level
165
+ below `subheadingTag` (`h3` for the default `h2`, `h4` for `h3`).
166
+
167
+ ---
168
+
169
+ ## Layout building blocks
170
+
171
+ The hero banner is a [GridStack](../../../../../.claude/skills/components/grid-stack.md) with two
172
+ stacked layers — an `image` layer (the service photo) behind a `content` layer (the scrim +
173
+ breadcrumb/title/pills). Each layer's root element is itself a
174
+ [PageRow](../../../../../.claude/skills/components/page-row.md): `.service-detail__hero-image-row`
175
+ (prop `heroImageVariant`, default `"full"`) wraps the photo, and `.service-detail__hero-overlay`
176
+ (prop `heroContentVariant`, default `"content"`) *is* the scrim/alignment element, not a wrapper
177
+ around one — a `PageRow`'s own box always spans the full width of its parent regardless of variant,
178
+ only its children default into the variant's grid column, so the one element can carry the
179
+ full-bleed scrim while its child (`.service-detail__hero-content`) still gets pulled to the
180
+ configured column. This means the photo bleeds to whatever width `.service-detail` itself is allowed
181
+ (edge-to-edge if `ServiceDetail` is placed without a content-constraining wrapper around it), while
182
+ the breadcrumb/title/pills always stay pulled into the page's normal content-width column,
183
+ regardless of how wide the photo bleeds. `--service-detail-hero-padding` now only controls
184
+ **block** (vertical) padding on the scrim — horizontal alignment of the text is handled entirely by
185
+ `.service-detail__hero-overlay`'s own `PageRow` grid, not by padding, so it can't fight the page's
186
+ content-grid gutters.
187
+
188
+ The two-column body (main content + sticky sidebar) is wrapped in its own `PageRow`, whose variant
189
+ is a prop: `bodyVariant` (`"full" | "popout" | "content" | "inset-content"`, default `"content"`).
190
+ The closing full-width CTA banner is wrapped the same way via `finalCtaVariant` (same type, same
191
+ `"content"` default). This means a consumer no longer needs to wrap `ServiceDetail` itself in an
192
+ outer `PageRow` purely to constrain either section's width — pass `body-variant`/`final-cta-variant`
193
+ directly. It's still fine (and necessary for a genuine full-bleed hero) to place `ServiceDetail`
194
+ inside an outer, wider `PageRow` — see
195
+ [service-summary.md](../../../../../.claude/skills/components/service-summary.md)-style nesting
196
+ notes in [page-row.md](../../../../../.claude/skills/components/page-row.md) for how nested page-rows
197
+ compose.
198
+
199
+ ---
200
+
163
201
  ## Text content — props, not CSS
164
202
 
165
203
  Section headings, sidebar labels, and CTA copy are **props**, not hardcoded strings:
@@ -177,6 +215,14 @@ Section headings, sidebar labels, and CTA copy are **props**, not hardcoded stri
177
215
  | `relatedServicesHeading` | `"You May Also Like"` |
178
216
  | `finalCtaHeading` | `"Ready to book your appointment?"` |
179
217
  | `finalCtaBody` | `"Get in touch to book your appointment."` |
218
+ | `pricePrefix` | `"From"` (hero price pill; `""` shows the price alone) |
219
+ | `breadcrumbAriaLabel` | Breadcrumb's own default |
220
+ | `idealForIcon` | `"mdi:diamond-stone"` (decorative, hidden from assistive tech) |
221
+
222
+ A section is left out, heading and all, when its data is empty (`process`, `idealFor`, `faqs`,
223
+ `maintenance`, a `heroHeading` with no text), and so is any heading or label prop set to `""`. Empty
224
+ `subtitle`, `title`, `price`, `duration`, `longDescription`, `whatIsIt` and FAQ questions/answers render
225
+ nothing, and empty breadcrumb labels are dropped.
180
226
 
181
227
  ```vue
182
228
  <ServiceDetail
@@ -226,6 +272,12 @@ never reach the hero pills or breadcrumb. Use the `--service-detail-hero-*` toke
226
272
 
227
273
  ---
228
274
 
275
+ ## Class passthrough
276
+
277
+ `style-class-passthrough` adds classes to the root `.service-detail`. Reactive after mount.
278
+
279
+ ---
280
+
229
281
  ## Notes
230
282
 
231
283
  - Routing is entirely delegated to the consumer via slots: `book-cta` (scoped `serviceData`),
@@ -251,4 +303,3 @@ never reach the hero pills or breadcrumb. Use the `--service-detail-hero-*` toke
251
303
  `@container` queries against the component's own width, not the viewport — they are not tokens
252
304
  since resizing them would need corresponding `minmax()`/`grid-template-columns` changes too, not
253
305
  just spacing (same rationale as `ServiceSummary`'s `--service-summary-grid-gap-desktop`).
254
-