srcdev-nuxt-components 9.6.0 → 9.6.2

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 (121) hide show
  1. package/.claude/skills/component-compliance-checklist.md +22 -3
  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/container-glow.md +21 -8
  7. package/.claude/skills/components/display-avatar.md +27 -10
  8. package/.claude/skills/components/display-chip.md +38 -24
  9. package/.claude/skills/components/display-pill.md +58 -10
  10. package/.claude/skills/components/display-prompt.md +8 -5
  11. package/.claude/skills/components/display-toast.md +21 -4
  12. package/.claude/skills/components/glowing-border.md +20 -2
  13. package/.claude/skills/components/google-reviews.md +2 -2
  14. package/.claude/skills/components/profile-section.md +59 -21
  15. package/.claude/skills/components/responsive-header.md +11 -1
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/site-header.md +2 -2
  18. package/.claude/skills/components/social-icons-list.md +2 -0
  19. package/.claude/skills/components/tabbed-content.md +300 -0
  20. package/.claude/skills/composable-anchor-scroll.md +16 -15
  21. package/.claude/skills/index.md +8 -7
  22. package/.claude/skills/storybook-add-story.md +55 -0
  23. package/.claude/skills/theming-component-token-pattern.md +48 -0
  24. package/.claude/skills/theming-status-tokens.md +60 -0
  25. package/.vscode/srcdev-component-display-avatar.code-snippets +9 -1
  26. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  27. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  28. package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
  29. package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
  30. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  31. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  32. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  33. package/app/assets/styles/setup/03.theming/index.css +1 -0
  34. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  35. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  36. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  37. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  38. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  39. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  40. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  41. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  42. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  43. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  44. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
  45. package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
  46. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  47. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  48. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  49. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  50. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  51. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  52. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  53. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  54. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  55. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  56. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  57. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  58. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +33 -15
  59. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +81 -36
  60. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +108 -6
  61. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +104 -13
  62. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +4 -3
  63. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +57 -19
  64. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  65. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  66. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  67. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  68. package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
  69. package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
  70. package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
  71. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
  72. package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
  73. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  74. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  75. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  76. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  77. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  78. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  79. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  80. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  81. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  82. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
  83. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
  84. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  85. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  86. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  87. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  88. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  89. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  90. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  91. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
  92. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
  93. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
  94. package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
  95. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
  96. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
  97. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
  98. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  99. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  100. package/app/composables/useTabs.ts +148 -206
  101. package/app/types/components/display-chip.d.ts +8 -4
  102. package/app/types/components/display-toast.d.ts +2 -0
  103. package/app/types/components/index.ts +0 -1
  104. package/app/types/components/select-menu.d.ts +2 -0
  105. package/app/utils/pausableTimeout.ts +41 -0
  106. package/nuxt.config.ts +1 -1
  107. package/package.json +1 -1
  108. package/.claude/skills/components/tab-navigation.md +0 -288
  109. package/.claude/skills/components/tabs-core.md +0 -187
  110. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  111. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  112. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  113. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  114. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  115. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
  116. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
  117. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
  118. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
  119. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  120. package/app/composables/useNavCollapse.ts +0 -86
  121. package/app/types/components/nav-item.d.ts +0 -11
@@ -2,35 +2,89 @@
2
2
 
3
3
  ## Public token API
4
4
 
5
+ ### Grid and picture
6
+
5
7
  | Token | Default | Controls |
6
8
  |---|---|---|
7
- | `--profile-section-gap` | `2rem` | Gap between picture and info, single column (below 768px) |
8
- | `--profile-section-gap-wide` | `4rem` | Gap between picture and info columns (768px+) |
9
- | `--profile-section-picture-width` | `384px` | Picture column width (768px+) |
9
+ | `--profile-section-gap` | `2rem` | Gap between picture and info, single column (component under 768px wide) |
10
+ | `--profile-section-gap-wide` | `4rem` | Gap between picture and info columns (component 768px wide or more) |
11
+ | `--profile-section-picture-width` | `38.4rem` | Maximum picture column width in the two-column layout. The column can shrink below it |
10
12
  | `--profile-section-picture-aspect-ratio` | `3 / 4` | Picture frame aspect ratio |
11
- | `--profile-section-picture-border-radius` | `8px` | Picture frame corner rounding |
13
+ | `--profile-section-picture-border-radius` | `0.8rem` | Picture frame corner rounding |
12
14
  | `--profile-section-info-block-gap` | `1.5rem` | Space after each `profile-info-N` block |
13
15
  | `--profile-section-links-gap` | `1rem` | Gap between items in the `profileLinks` slot |
16
+ | `--profile-section-info-text-colour` | `inherit` | Text colour of the info column (blocks and links) |
17
+
18
+ ### Info block highlights
19
+
20
+ | Token | Default | Controls |
21
+ |---|---|---|
22
+ | `--profile-section-highlight-colour` | `var(--colour-text-accent)` | `.highlight` inside a `.location` element |
23
+ | `--profile-section-link-colour` | `var(--colour-link-default)` | `.highlight` inside a `.services` element |
24
+ | `--profile-section-link-colour-hover` | `var(--colour-link-hover)` | The same, on hover |
25
+ | `--profile-section-highlight-font-weight` | `600` | Weight of both highlight kinds (also sets the `wght` variation axis) |
26
+
27
+ The defaults read the global `--colour-*` tokens, so setting those on an ancestor still works.
14
28
 
15
- Highlight colours inside info blocks read the global `--colour-text-accent`,
16
- `--colour-link-default` and `--colour-link-hover` (see **State hooks**).
29
+ ### Header in picture
17
30
 
31
+ These only apply when `header-placement` is `picture-bottom` or `picture-top`. The scrim fades
32
+ upward from the bottom edge for `picture-bottom` and downward from the top edge for `picture-top`.
33
+
34
+ | Token | Default | Controls |
35
+ |---|---|---|
36
+ | `--profile-section-picture-header-scrim-colour` | `#000` | Colour the scrim starts from at the frame's edge |
37
+ | `--profile-section-picture-header-scrim-opacity` | `75%` | Opacity of the scrim colour at its start |
38
+ | `--profile-section-picture-header-scrim-solid-stop` | `0%` | How far into the header the scrim stays at full strength before it starts to fade (e.g. `55%` keeps it solid behind the text) |
39
+ | `--profile-section-picture-header-scrim-target-colour` | `transparent` | Colour the scrim fades into at the header's inner edge. Set it to roughly the photo's colour there (often translucent) so the fade blends into the image instead of leaving a grey band or a visible edge |
40
+ | `--profile-section-picture-header-scrim` | gradient from the scrim colour | The whole scrim `background`; set `none` to remove it |
41
+ | `--profile-section-picture-header-text-colour` | `contrast-color(<scrim colour>)`, `#fff` without support | Eyebrow and heading text colour |
42
+ | `--profile-section-picture-header-eyebrow-colour` | `inherit` | Eyebrow colour only |
43
+ | `--profile-section-picture-header-padding` | `2.4rem` | Padding around the header inside the picture |
44
+ | `--profile-section-picture-header-gap` | `0.4rem` | Gap between eyebrow and heading |
45
+
46
+ Matching the photo is a judgement call per image: pick the target colour from the part of the photo
47
+ the fade ends on, and add end padding (`--profile-section-picture-header-padding`, e.g.
48
+ `3.2rem 3.2rem 6.4rem` for `picture-top`) so the fade sits past the text. The header only rounds the
49
+ corners on the frame's edge, so an opaque target colour doesn't show rounded inner corners.
50
+ Browsers without `color-mix()` (before Safari 16.2) start the scrim from 75% black whatever the scrim
51
+ colour and opacity; the solid stop and target colour still apply.
52
+
53
+ `contrast-color()` only reads the scrim colour, not the photo. If you remove the scrim, set
54
+ `--profile-section-picture-header-text-colour` yourself to suit the image. The eyebrow's own
55
+ `--eyebrow-text-bg-img` is set to `none` inside the picture.
56
+
57
+ > Changed 2026-10-14: new `header-placement` prop and its tokens, and `--profile-section-info-text-colour`. The scrim
58
+ > gained opacity, solid-stop and target-colour tokens. The image is now absolutely positioned
59
+ > inside the frame (no visual change), so a header in the frame can grow it past its aspect ratio.
60
+ >
61
+ > Changed 2026-10-14: the highlight colours read the global `--colour-*` tokens directly; they now
62
+ > go through the `--profile-section-*` tokens above, with the same defaults. `384px` and `8px`
63
+ > became the same values in rem.
64
+ >
18
65
  > Changed 2026-09-27: all of these were hardcoded values; they're now tokens with the same
19
66
  > defaults.
20
67
 
21
- No private `--_` tokens.
68
+ Private tokens (not public API): `--_scrim-colour`, the resolved scrim colour used by the gradient
69
+ and `contrast-color()`, `--_scrim-direction` (`to top` or `to bottom`, set by the placement), and
70
+ `--_scrim-start-colour`, `--_scrim-solid-stop` and `--_scrim-target-colour`, which compose the
71
+ gradient from the tokens above.
22
72
 
23
73
  ---
24
74
 
25
75
  ## State hooks
26
76
 
27
- Inner classes: `.profile-section-header`, `.profile-section-inner` (the grid),
28
- `.profile-section-picture` (frame), `.profile-section-image` (the `NuxtImg`),
29
- `.profile-section-info`, `.profile-section-info-content`, `.profile-section-info-block` (one per
30
- `profile-info-N` slot), `.profile-section-links`.
77
+ Inner classes: `.profile-section-header` (only rendered when the `eyebrowText` or `heroText` slot
78
+ is given; it moves inside `.profile-section-picture`, before the image, when `header-placement` is
79
+ `picture-bottom` or `picture-top`), `.profile-section-inner` (the grid), `.profile-section-picture` (frame),
80
+ `.profile-section-image` (the `NuxtImg`), `.profile-section-info`,
81
+ `.profile-section-info-content`, `.profile-section-info-block` (one per `profile-info-N` slot
82
+ given), `.profile-section-links`.
31
83
 
32
- Content hooks inside info blocks: an element with class `location` or `services` containing a
33
- `.highlight` gets a bold accent (`location`) or link (`services`) colour.
84
+ `data-header-placement` on the root: `above`, `picture-bottom` or `picture-top`.
85
+
86
+ Content hooks inside info blocks: a `.highlight` inside an element with class `location` or
87
+ `services` gets the highlight or link colour above.
34
88
 
35
89
  > Changed 2026-09-27: renamed from `.picture`, `.profile-picture`, `.profile-info`,
36
90
  > `.profile-info-content`, `.profile-info-block` and `.profile-links`. `.picture` in particular
@@ -38,6 +92,35 @@ Content hooks inside info blocks: an element with class `location` or `services`
38
92
 
39
93
  ---
40
94
 
95
+ ## Layout
96
+
97
+ The root is a size container named `profile-section` (`container: profile-section / inline-size`).
98
+ The grid switches to two columns when **the component itself** is 768px wide or more, not the
99
+ viewport, so it stays stacked in a narrow column or sidebar. Because it's an inline-size container,
100
+ its width can't come from its content: in a flex row or other shrink-to-fit context, give it a
101
+ width (`flex: 1`, `width: 100%`) or it collapses.
102
+
103
+ Both grid columns are `minmax(0, …)`, so long unbroken text wraps (the root sets
104
+ `overflow-wrap: anywhere`) instead of widening the grid. Profile links wrap onto more lines.
105
+
106
+ To target the two-column layout in your own CSS, query the same container:
107
+
108
+ ```css
109
+ .my-page .profile-section-inner {
110
+ @container profile-section (min-width: 768px) {
111
+ align-items: center;
112
+ }
113
+ }
114
+ ```
115
+
116
+ > Changed 2026-10-14: the breakpoint was a viewport `@media (min-width: 768px)` query. A
117
+ > `@media (min-width: 768px)` override of `.profile-section-inner`'s `grid-template-columns` now
118
+ > applies at a different point from the component's own switch. Remove it and set
119
+ > `--profile-section-picture-width` instead (`minmax(0, 320px) 1fr` is
120
+ > `--profile-section-picture-width: 32rem`, since the column is now `minmax(0, …)` by default).
121
+
122
+ ---
123
+
41
124
  ## Local overrides
42
125
 
43
126
  Set the tokens above on an element you own (a page or section class, or a class added with
@@ -51,3 +134,10 @@ Set the tokens above on an element you own (a page or section class, or a class
51
134
  ## Class passthrough
52
135
 
53
136
  `style-class-passthrough` adds classes to the root element. Reactive after mount.
137
+
138
+ ---
139
+
140
+ ## Notes
141
+
142
+ There are no line-clamp tokens. All text comes through slots, so it is your own markup: clamp it
143
+ there, for example with `-webkit-line-clamp` on your own paragraph class.
@@ -1,17 +1,27 @@
1
1
  <template>
2
- <component :is="tag" class="profile-section" :class="[elementClasses]" :aria-labelledby="ariaLabelledby">
3
- <header class="profile-section-header">
2
+ <component
3
+ :is="tag"
4
+ class="profile-section"
5
+ :class="[elementClasses]"
6
+ :aria-labelledby="ariaLabelledby"
7
+ :data-header-placement="headerPlacement"
8
+ >
9
+ <header v-if="hasHeader() && !headerInPicture" class="profile-section-header">
4
10
  <slot v-if="hasEyebrowTextSlot()" name="eyebrowText"></slot>
5
11
  <slot v-if="hasHeroTextSlot()" name="heroText" :heading-id="headingId"></slot>
6
12
  </header>
7
13
 
8
14
  <div class="profile-section-inner">
9
15
  <div class="profile-section-picture">
16
+ <header v-if="hasHeader() && headerInPicture" class="profile-section-header">
17
+ <slot v-if="hasEyebrowTextSlot()" name="eyebrowText"></slot>
18
+ <slot v-if="hasHeroTextSlot()" name="heroText" :heading-id="headingId"></slot>
19
+ </header>
10
20
  <NuxtImg
11
21
  :src="profilePicture.src"
12
22
  :alt="profilePicture.alt"
13
- :width="profilePicture.width ?? 828"
14
- :height="profilePicture.height ?? 1104"
23
+ :width="imageWidth"
24
+ :height="imageHeight"
15
25
  class="profile-section-image"
16
26
  />
17
27
  </div>
@@ -36,12 +46,12 @@ import type { ProfilePicture } from "~/types/components";
36
46
  interface Props {
37
47
  tag?: "div" | "section" | "article" | "main";
38
48
  profilePicture: ProfilePicture;
39
- profileInfoCount?: number;
49
+ headerPlacement?: "above" | "picture-bottom" | "picture-top";
40
50
  styleClassPassthrough?: string | string[];
41
51
  }
42
52
  const props = withDefaults(defineProps<Props>(), {
43
53
  tag: "div",
44
- profileInfoCount: 3,
54
+ headerPlacement: "above",
45
55
  styleClassPassthrough: () => [],
46
56
  });
47
57
 
@@ -50,16 +60,21 @@ const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
50
60
  const slots = useSlots();
51
61
  const hasEyebrowTextSlot = () => Boolean(slots.eyebrowText);
52
62
  const hasHeroTextSlot = () => Boolean(slots.heroText);
63
+ const hasHeader = () => hasEyebrowTextSlot() || hasHeroTextSlot();
53
64
  const hasProfileLinksSlot = () => Boolean(slots.profileLinks);
54
65
 
55
- const profileInfoSlots = () => {
56
- const provided = Object.keys(slots)
66
+ const headerInPicture = computed(() => props.headerPlacement !== "above");
67
+
68
+ const profileInfoSlots = () =>
69
+ Object.keys(slots)
57
70
  .filter((key) => /^profile-info-\d+$/.test(key))
58
71
  .sort((a, b) => parseInt(a.split("-")[2] ?? "0") - parseInt(b.split("-")[2] ?? "0"));
59
- return provided.length > 0
60
- ? provided
61
- : Array.from({ length: props.profileInfoCount }, (_, i) => `profile-info-${i + 1}`);
62
- };
72
+
73
+ const positiveOr = (value: number | undefined, fallback: number) =>
74
+ typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.round(value) : fallback;
75
+
76
+ const imageWidth = computed(() => positiveOr(props.profilePicture.width, 828));
77
+ const imageHeight = computed(() => positiveOr(props.profilePicture.height, 1104));
63
78
 
64
79
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
65
80
 
@@ -74,57 +89,142 @@ watch(
74
89
  <style lang="css">
75
90
  @layer components {
76
91
  .profile-section {
92
+ container: profile-section / inline-size;
93
+ overflow-wrap: anywhere;
94
+
77
95
  .profile-section-inner {
78
96
  display: grid;
79
- grid-template-columns: 1fr;
97
+ grid-template-columns: minmax(0, 1fr);
80
98
  gap: var(--profile-section-gap, 2rem);
81
99
 
82
- @media (min-width: 768px) {
83
- grid-template-columns: var(--profile-section-picture-width, 384px) 1fr;
100
+ @container profile-section (min-width: 768px) {
101
+ grid-template-columns: minmax(0, var(--profile-section-picture-width, 38.4rem)) minmax(0, 1fr);
84
102
  align-items: start;
85
103
  gap: var(--profile-section-gap-wide, 4rem);
86
104
  }
87
105
 
88
106
  .profile-section-picture {
107
+ position: relative;
89
108
  aspect-ratio: var(--profile-section-picture-aspect-ratio, 3 / 4);
90
- border-radius: var(--profile-section-picture-border-radius, 8px);
109
+ border-radius: var(--profile-section-picture-border-radius, 0.8rem);
91
110
  overflow: hidden;
92
111
 
93
112
  .profile-section-image {
113
+ position: absolute;
114
+ inset: 0;
94
115
  object-fit: cover;
95
116
  width: 100%;
96
117
  height: 100%;
118
+ border-radius: inherit;
97
119
  }
98
120
  }
99
121
 
122
+ .profile-section-info {
123
+ color: var(--profile-section-info-text-colour, inherit);
124
+ }
125
+
100
126
  .profile-section-info-block {
101
127
  margin-block-end: var(--profile-section-info-block-gap, 1.5rem);
102
128
 
129
+ .location .highlight,
130
+ .services .highlight {
131
+ font-weight: var(--profile-section-highlight-font-weight, 600);
132
+ font-variation-settings: "wght" var(--profile-section-highlight-font-weight, 600);
133
+ }
134
+
103
135
  .location .highlight {
104
- color: var(--colour-text-accent);
105
- font-weight: 600;
106
- font-variation-settings: "wght" 600;
136
+ color: var(--profile-section-highlight-colour, var(--colour-text-accent));
107
137
  }
108
138
 
109
139
  .services .highlight {
110
- color: var(--colour-link-default);
111
- font-weight: 600;
112
- font-variation-settings: "wght" 600;
140
+ color: var(--profile-section-link-colour, var(--colour-link-default));
113
141
 
114
142
  &:hover {
115
- color: var(--colour-link-hover);
143
+ color: var(--profile-section-link-colour-hover, var(--colour-link-hover));
116
144
  }
117
145
  }
118
146
  }
119
147
 
120
148
  .profile-section-links {
121
149
  display: flex;
150
+ flex-wrap: wrap;
122
151
  flex-grow: 1;
123
152
  gap: var(--profile-section-links-gap, 1rem);
124
153
  align-items: end;
125
154
  justify-content: flex-end;
126
155
  }
127
156
  }
157
+
158
+ &:is([data-header-placement="picture-bottom"], [data-header-placement="picture-top"]) .profile-section-picture {
159
+ /* Visible overflow keeps aspect-ratio a minimum, so a long header grows the frame instead of being clipped. */
160
+ --_scrim-colour: var(--profile-section-picture-header-scrim-colour, #000);
161
+ --_scrim-direction: to top;
162
+ --_scrim-start-colour: color-mix(
163
+ in oklab,
164
+ var(--_scrim-colour) var(--profile-section-picture-header-scrim-opacity, 75%),
165
+ transparent
166
+ );
167
+ --_scrim-solid-stop: var(--profile-section-picture-header-scrim-solid-stop, 0%);
168
+ --_scrim-target-colour: var(--profile-section-picture-header-scrim-target-colour, transparent);
169
+
170
+ display: flex;
171
+ flex-direction: column;
172
+ justify-content: flex-end;
173
+ overflow: visible;
174
+
175
+ .profile-section-header {
176
+ position: relative;
177
+ z-index: 1;
178
+ display: grid;
179
+ gap: var(--profile-section-picture-header-gap, 0.4rem);
180
+ padding: var(--profile-section-picture-header-padding, 2.4rem);
181
+ border-end-start-radius: inherit;
182
+ border-end-end-radius: inherit;
183
+ color: var(--profile-section-picture-header-text-colour, #fff);
184
+ background: var(
185
+ --profile-section-picture-header-scrim,
186
+ linear-gradient(
187
+ var(--_scrim-direction),
188
+ rgb(0 0 0 / 0.75),
189
+ rgb(0 0 0 / 0.75) var(--_scrim-solid-stop),
190
+ var(--_scrim-target-colour)
191
+ )
192
+ );
193
+
194
+ @supports (color: color-mix(in oklab, red, transparent)) {
195
+ background: var(
196
+ --profile-section-picture-header-scrim,
197
+ linear-gradient(
198
+ var(--_scrim-direction),
199
+ var(--_scrim-start-colour),
200
+ var(--_scrim-start-colour) var(--_scrim-solid-stop),
201
+ var(--_scrim-target-colour)
202
+ )
203
+ );
204
+ }
205
+
206
+ @supports (color: contrast-color(red)) {
207
+ color: var(--profile-section-picture-header-text-colour, contrast-color(var(--_scrim-colour)));
208
+ }
209
+
210
+ .eyebrow-text {
211
+ --eyebrow-text-bg-img: none;
212
+ color: var(--profile-section-picture-header-eyebrow-colour, inherit);
213
+ }
214
+ }
215
+ }
216
+
217
+ &[data-header-placement="picture-top"] .profile-section-picture {
218
+ --_scrim-direction: to bottom;
219
+
220
+ justify-content: flex-start;
221
+
222
+ .profile-section-header {
223
+ border-radius: 0;
224
+ border-start-start-radius: inherit;
225
+ border-start-end-radius: inherit;
226
+ }
227
+ }
128
228
  }
129
229
  }
130
230
  </style>