astro-component-kit 0.3.1 → 0.4.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 (249) hide show
  1. package/README.md +133 -36
  2. package/package.json +17 -4
  3. package/src/components/index.ts +18 -202
  4. package/src/components/lib/Galaxy3D.astro +249 -137
  5. package/src/components/lib/app/AppSidebar.astro +249 -0
  6. package/src/components/lib/app/AppTopbar.astro +284 -0
  7. package/src/components/lib/app/ArrivalsBoard.astro +176 -0
  8. package/src/components/lib/app/AuthCard.astro +213 -0
  9. package/src/components/lib/app/DataTable.astro +346 -0
  10. package/src/components/lib/app/GuestProfileCard.astro +133 -0
  11. package/src/components/lib/app/HousekeepingBoard.astro +189 -0
  12. package/src/components/lib/app/PageHeader.astro +157 -0
  13. package/src/components/lib/app/PlanUsageCard.astro +112 -0
  14. package/src/components/lib/app/RoomRack.astro +207 -0
  15. package/src/components/lib/app/SettingsSection.astro +96 -0
  16. package/src/components/lib/app/SwitchRow.astro +71 -0
  17. package/src/components/lib/app/TeamMembers.astro +190 -0
  18. package/src/components/lib/app/app-icons.ts +52 -0
  19. package/src/components/lib/app/index.ts +19 -0
  20. package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
  21. package/src/components/lib/buttons/RippleButton.astro +199 -0
  22. package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
  23. package/src/components/lib/buttons/index.ts +4 -0
  24. package/src/components/lib/cards/GithubRepoCard.astro +176 -0
  25. package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
  26. package/src/components/lib/cards/StatsTrendCard.astro +149 -0
  27. package/src/components/lib/cards/index.ts +3 -0
  28. package/src/components/lib/charts/ActivityRings.astro +105 -0
  29. package/src/components/lib/charts/AreaChart.astro +4 -4
  30. package/src/components/lib/charts/BarChart.astro +3 -3
  31. package/src/components/lib/charts/BoxPlotChart.astro +109 -0
  32. package/src/components/lib/charts/BubbleChart.astro +101 -0
  33. package/src/components/lib/charts/BulletChart.astro +160 -0
  34. package/src/components/lib/charts/BumpChart.astro +92 -0
  35. package/src/components/lib/charts/CandlestickChart.astro +105 -0
  36. package/src/components/lib/charts/ChartFrame.astro +373 -0
  37. package/src/components/lib/charts/DivergingBarChart.astro +98 -0
  38. package/src/components/lib/charts/DonutChart.astro +2 -2
  39. package/src/components/lib/charts/DumbbellChart.astro +92 -0
  40. package/src/components/lib/charts/ForecastLineChart.astro +125 -0
  41. package/src/components/lib/charts/FunnelChart.astro +122 -0
  42. package/src/components/lib/charts/GaugeChart.astro +111 -0
  43. package/src/components/lib/charts/GoalProgressChart.astro +97 -0
  44. package/src/components/lib/charts/GroupedBarChart.astro +125 -0
  45. package/src/components/lib/charts/HeatmapChart.astro +131 -0
  46. package/src/components/lib/charts/HistogramChart.astro +111 -0
  47. package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
  48. package/src/components/lib/charts/KpiStatTile.astro +116 -0
  49. package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
  50. package/src/components/lib/charts/LineChart.astro +160 -0
  51. package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
  52. package/src/components/lib/charts/LollipopChart.astro +100 -0
  53. package/src/components/lib/charts/MeterGauge.astro +122 -0
  54. package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
  55. package/src/components/lib/charts/PieChart.astro +135 -0
  56. package/src/components/lib/charts/PolarAreaChart.astro +85 -0
  57. package/src/components/lib/charts/PopulationPyramid.astro +122 -0
  58. package/src/components/lib/charts/PunchCardChart.astro +89 -0
  59. package/src/components/lib/charts/RadarChart.astro +94 -0
  60. package/src/components/lib/charts/RadarMetricChart.astro +168 -0
  61. package/src/components/lib/charts/RadialBarChart.astro +124 -0
  62. package/src/components/lib/charts/RangeAreaChart.astro +113 -0
  63. package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
  64. package/src/components/lib/charts/SankeyChart.astro +128 -0
  65. package/src/components/lib/charts/ScatterPlot.astro +139 -0
  66. package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
  67. package/src/components/lib/charts/SlopeChart.astro +105 -0
  68. package/src/components/lib/charts/SparklineChart.astro +87 -0
  69. package/src/components/lib/charts/StackedAreaChart.astro +130 -0
  70. package/src/components/lib/charts/StackedBarChart.astro +136 -0
  71. package/src/components/lib/charts/SunburstChart.astro +103 -0
  72. package/src/components/lib/charts/TreemapChart.astro +94 -0
  73. package/src/components/lib/charts/WaffleChart.astro +121 -0
  74. package/src/components/lib/charts/WaterfallChart.astro +118 -0
  75. package/src/components/lib/charts/chart-tooltip.ts +75 -0
  76. package/src/components/lib/charts/chart-utils.ts +271 -0
  77. package/src/components/lib/charts/index.ts +46 -0
  78. package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
  79. package/src/components/lib/data/MetricComparisonRow.astro +109 -0
  80. package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
  81. package/src/components/lib/data/index.ts +3 -0
  82. package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
  83. package/src/components/lib/ecommerce/CartPageList.astro +290 -0
  84. package/src/components/lib/ecommerce/CartToast.astro +276 -0
  85. package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
  86. package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
  87. package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
  88. package/src/components/lib/ecommerce/FilterBar.astro +278 -0
  89. package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
  90. package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
  91. package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
  92. package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
  93. package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
  94. package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
  95. package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
  96. package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
  97. package/src/components/lib/ecommerce/ProductRating.astro +78 -0
  98. package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
  99. package/src/components/lib/ecommerce/PromoBar.astro +145 -0
  100. package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
  101. package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
  102. package/src/components/lib/ecommerce/StoreHero.astro +260 -0
  103. package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
  104. package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
  105. package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
  106. package/src/components/lib/ecommerce/cart-store.ts +151 -0
  107. package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
  108. package/src/components/lib/ecommerce/index.ts +34 -0
  109. package/src/components/lib/ecommerce/product-icons.ts +28 -0
  110. package/src/components/lib/ecommerce/types.ts +70 -0
  111. package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
  112. package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
  113. package/src/components/lib/feedback/index.ts +2 -0
  114. package/src/components/lib/forms/AnimatedInput.astro +4 -2
  115. package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
  116. package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
  117. package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
  118. package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
  119. package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
  120. package/src/components/lib/forms/index.ts +5 -0
  121. package/src/components/lib/index.ts +2 -0
  122. package/src/components/lib/layout/BentoGrid3.astro +6 -4
  123. package/src/components/lib/layout/CenterFixedBox.astro +2 -2
  124. package/src/components/lib/layout/CenteredContainer.astro +4 -2
  125. package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
  126. package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
  127. package/src/components/lib/layout/FeatureHero.astro +4 -2
  128. package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
  129. package/src/components/lib/layout/FullBleedSection.astro +2 -2
  130. package/src/components/lib/layout/GalleryGrid4.astro +2 -2
  131. package/src/components/lib/layout/HolyGrailGrid.astro +127 -0
  132. package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
  133. package/src/components/lib/layout/MasonryGrid.astro +2 -2
  134. package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
  135. package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
  136. package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
  137. package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
  138. package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
  139. package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
  140. package/src/components/lib/layout/index.ts +3 -1
  141. package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
  142. package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
  143. package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
  144. package/src/components/lib/navigation/index.ts +3 -0
  145. package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
  146. package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
  147. package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
  148. package/src/components/lib/overlays/index.ts +3 -0
  149. package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
  150. package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
  151. package/src/components/lib/sections/AgencyHeader.astro +161 -0
  152. package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
  153. package/src/components/lib/sections/AgencyServicesList.astro +113 -0
  154. package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
  155. package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
  156. package/src/components/lib/sections/BlogGridSection.astro +133 -0
  157. package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
  158. package/src/components/lib/sections/CardsShowcase.astro +28 -26
  159. package/src/components/lib/sections/CareersOpenings.astro +141 -0
  160. package/src/components/lib/sections/ChangelogSection.astro +151 -0
  161. package/src/components/lib/sections/ContactGlassForm.astro +10 -8
  162. package/src/components/lib/sections/ContactSplitMap.astro +141 -0
  163. package/src/components/lib/sections/CtaAppDownload.astro +13 -11
  164. package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
  165. package/src/components/lib/sections/CtaNewsletterSection.astro +165 -0
  166. package/src/components/lib/sections/CtaSplitImage.astro +149 -0
  167. package/src/components/lib/sections/DataShowcase.astro +23 -21
  168. package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
  169. package/src/components/lib/sections/FaqTwoColumn.astro +121 -0
  170. package/src/components/lib/sections/FeatureAlternatingRows.astro +192 -0
  171. package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
  172. package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
  173. package/src/components/lib/sections/FeatureTabsShowcase.astro +182 -0
  174. package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
  175. package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
  176. package/src/components/lib/sections/FooterMegaSection.astro +157 -0
  177. package/src/components/lib/sections/FormsShowcase.astro +22 -20
  178. package/src/components/lib/sections/HeroDashboardPreview.astro +182 -0
  179. package/src/components/lib/sections/HeroGlass.astro +22 -20
  180. package/src/components/lib/sections/HeroGradient.astro +15 -13
  181. package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
  182. package/src/components/lib/sections/HeroShowcaseSplit.astro +306 -0
  183. package/src/components/lib/sections/HeroSplit.astro +13 -11
  184. package/src/components/lib/sections/HeroTerminal.astro +253 -0
  185. package/src/components/lib/sections/HeroWaitlist.astro +231 -0
  186. package/src/components/lib/sections/HowItWorksSteps.astro +180 -0
  187. package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
  188. package/src/components/lib/sections/LayoutShowcase.astro +21 -19
  189. package/src/components/lib/sections/LogoCloudSection.astro +96 -0
  190. package/src/components/lib/sections/LogoWallGrid.astro +93 -0
  191. package/src/components/lib/sections/NavigationShowcase.astro +15 -13
  192. package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
  193. package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
  194. package/src/components/lib/sections/PricingSinglePlan.astro +159 -0
  195. package/src/components/lib/sections/PricingTierCards.astro +11 -9
  196. package/src/components/lib/sections/PricingToggleCards.astro +171 -0
  197. package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
  198. package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
  199. package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
  200. package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
  201. package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
  202. package/src/components/lib/sections/ShopLookbook.astro +250 -0
  203. package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
  204. package/src/components/lib/sections/ShopProductRail.astro +150 -0
  205. package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
  206. package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
  207. package/src/components/lib/sections/StatsCounterBand.astro +124 -0
  208. package/src/components/lib/sections/StatsDashboardSection.astro +85 -0
  209. package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
  210. package/src/components/lib/sections/TeamGridSection.astro +12 -10
  211. package/src/components/lib/sections/TestimonialCarousel.astro +167 -0
  212. package/src/components/lib/sections/TestimonialFeatured.astro +122 -0
  213. package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
  214. package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
  215. package/src/components/lib/sections/TypographyShowcase.astro +10 -8
  216. package/src/components/lib/sections/index.ts +39 -0
  217. package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
  218. package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
  219. package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
  220. package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
  221. package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
  222. package/src/components/lib/templates/ecommerce/data.ts +187 -0
  223. package/src/components/lib/templates/ecommerce/index.ts +14 -0
  224. package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
  225. package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
  226. package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
  227. package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
  228. package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
  229. package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
  230. package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
  231. package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
  232. package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
  233. package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
  234. package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
  235. package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
  236. package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
  237. package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
  238. package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
  239. package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
  240. package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
  241. package/src/components/lib/templates/saas-hotel/data.ts +229 -0
  242. package/src/components/lib/templates/saas-hotel/index.ts +19 -0
  243. package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
  244. package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
  245. package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
  246. package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
  247. package/src/components/lib/typography/HackerDecodeText.astro +85 -0
  248. package/src/components/lib/typography/index.ts +4 -0
  249. package/src/styles/global.css +113 -1
@@ -0,0 +1,91 @@
1
+ ---
2
+ /**
3
+ * ShopValuesMarquee — Brand promise band: an oversized italic marquee followed by numbered
4
+ * value columns (materials, making, after-care…).
5
+ *
6
+ * @param {string[]} words - Marquee words. Empty hides the marquee.
7
+ * @param {Array<{title: string, text: string}>} values - 2–4 value columns.
8
+ * @param {number} speed - Marquee loop duration in seconds. Default 36.
9
+ * @param {string} variant - Style (plain|inverted). Default 'plain'.
10
+ * @param {string} size - Marquee scale (sm|md|lg). Default 'md'.
11
+ * @param {string} className - Extra class.
12
+ *
13
+ * Accessibility: the marquee is decorative (aria-hidden) and pauses on hover and with reduced
14
+ * motion; values are a real list with headings.
15
+ */
16
+ interface Props {
17
+ words?: string[];
18
+ values?: Array<{ title: string; text: string }>;
19
+ speed?: number;
20
+ variant?: 'plain' | 'inverted';
21
+ size?: 'sm' | 'md' | 'lg';
22
+ className?: string;
23
+ }
24
+
25
+ const {
26
+ words = ['Made slowly', 'Natural fibres', 'Small workshops', 'Repaired for life'],
27
+ values = [
28
+ { title: 'Honest materials', text: 'Linen, wool, organic cotton and stoneware — traceable to the mill and the kiln.' },
29
+ { title: 'Made in small runs', text: 'Each piece is produced in batches of a few hundred, never mass-manufactured.' },
30
+ { title: 'Kept for years', text: 'Free repairs on everything we make, and a buy-back for pieces you have outgrown.' },
31
+ ],
32
+ speed = 36,
33
+ variant = 'plain',
34
+ size = 'md',
35
+ className = '',
36
+ } = Astro.props;
37
+ ---
38
+
39
+ <section class:list={['svm', `svm--${variant}`, `svm--${size}`, className]} style={`--svm-speed: ${speed}s`}>
40
+ {words.length > 0 && (
41
+ <div class="svm__marquee" aria-hidden="true">
42
+ <div class="svm__track">
43
+ {[0, 1].map(() => words.map((w) => <span>{w}<i>✺</i></span>))}
44
+ </div>
45
+ </div>
46
+ )}
47
+ {values.length > 0 && (
48
+ <ol class="svm__values">
49
+ {values.map((v, i) => (
50
+ <li>
51
+ <span class="svm__num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
52
+ <h3>{v.title}</h3>
53
+ <p>{v.text}</p>
54
+ </li>
55
+ ))}
56
+ </ol>
57
+ )}
58
+ </section>
59
+
60
+ <style>
61
+ .svm { container-type: inline-size; --svm-fs: clamp(3rem, 9vw, 7.5rem); color: var(--c-text-1, #fff); overflow: hidden; }
62
+ .svm--sm { --svm-fs: clamp(2rem, 6vw, 4.5rem); }
63
+ .svm--lg { --svm-fs: clamp(4rem, 12vw, 10rem); }
64
+ .svm--inverted { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
65
+ .svm__marquee { border-block: 1px solid currentColor; overflow: hidden; }
66
+ .svm__track { display: flex; width: max-content; animation: svm-run var(--svm-speed) linear infinite; }
67
+ .svm:hover .svm__track { animation-play-state: paused; }
68
+ .svm__track span { display: inline-flex; align-items: center; gap: 0.35em; padding: 0.1em 0 0.15em 0.35em; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: var(--svm-fs); font-style: italic; letter-spacing: -0.04em; line-height: 1.05; white-space: nowrap; }
69
+ .svm__track i { font-style: normal; font-size: 0.4em; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
70
+ @keyframes svm-run { to { transform: translateX(-50%); } }
71
+
72
+ .svm__values {
73
+ display: grid;
74
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
75
+ margin: 0 auto;
76
+ max-width: 1440px;
77
+ padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 3vw, 2.5rem);
78
+ list-style: none;
79
+ gap: 0;
80
+ }
81
+ .svm__values li { padding: 0 var(--sp-6, 1.5rem) var(--sp-4, 1rem); border-left: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); }
82
+ .svm--inverted .svm__values li { border-left-color: rgb(var(--paper-rgb, 5 5 5) / 0.2); }
83
+ .svm__num { font-size: 0.75rem; font-variant-numeric: tabular-nums; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
84
+ .svm__values h3 { margin: var(--sp-3, 0.75rem) 0 var(--sp-2, 0.5rem); font-family: var(--font-display, Georgia, serif); font-size: 1.5rem; font-weight: 400; letter-spacing: -0.02em; }
85
+ .svm__values p { margin: 0; max-width: 22rem; font-size: 0.92rem; line-height: 1.65; opacity: 0.75; }
86
+
87
+ @container (max-width: 640px) {
88
+ .svm__values li { padding: var(--sp-4, 1rem) 0; border-left: 0; border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); }
89
+ }
90
+ @media (prefers-reduced-motion: reduce) { .svm__track { animation: none; } }
91
+ </style>
@@ -0,0 +1,124 @@
1
+ ---
2
+ /**
3
+ * StatsCounterBand — Full-width band of headline numbers that count up when
4
+ * scrolled into view. Final values are in the HTML, so no-JS and screen
5
+ * readers always get the real number; motion respects reduced-motion.
6
+ *
7
+ * @param {string} title - Optional heading (visually hidden when `hideTitle`).
8
+ * @param {boolean} hideTitle - Visually hide the heading.
9
+ * @param {Array<{value: number, prefix?: string, suffix?: string, label: string, decimals?: number}>} stats - Numbers.
10
+ * @param {string} variant - Band style (divided|cards|plain).
11
+ * @param {string} size - Vertical rhythm (sm|md|lg).
12
+ * @param {string} className - Extra CSS class.
13
+ */
14
+ interface Stat { value: number; prefix?: string; suffix?: string; label: string; decimals?: number }
15
+ interface Props {
16
+ title?: string;
17
+ hideTitle?: boolean;
18
+ stats?: Stat[];
19
+ variant?: 'divided' | 'cards' | 'plain';
20
+ size?: 'sm' | 'md' | 'lg';
21
+ className?: string;
22
+ }
23
+
24
+ const {
25
+ title = 'Trusted at scale',
26
+ hideTitle = false,
27
+ stats = [
28
+ { value: 12000, suffix: '+', label: 'Developers using the kit' },
29
+ { value: 250, suffix: '+', label: 'Components & sections' },
30
+ { value: 99.98, suffix: '%', label: 'CDN uptime last 12 months', decimals: 2 },
31
+ { value: 48, prefix: '<', suffix: 'ms', label: 'Median time to first byte' },
32
+ ],
33
+ variant = 'divided',
34
+ size = 'md',
35
+ className = '',
36
+ } = Astro.props;
37
+
38
+ const id = `scb-${Math.random().toString(36).slice(2, 8)}`;
39
+ const fmt = (s: Stat) => `${s.prefix ?? ''}${s.value.toLocaleString('en-US', { minimumFractionDigits: s.decimals ?? 0, maximumFractionDigits: s.decimals ?? 0 })}${s.suffix ?? ''}`;
40
+ ---
41
+
42
+ <div class="cq-stats-counter-band-cq">
43
+ <section class={`scb scb--${variant} scb--${size} ${className}`} aria-labelledby={id}>
44
+ <div class="scb__inner">
45
+ <h2 id={id} class:list={['scb__title', { 'scb__sr': hideTitle }]}>{title}</h2>
46
+ <dl class="scb__grid">
47
+ {stats.map((s) => (
48
+ <div class="scb__item">
49
+ <dt class="scb__label">{s.label}</dt>
50
+ <dd class="scb__value" data-count={s.value} data-decimals={s.decimals ?? 0} data-prefix={s.prefix ?? ''} data-suffix={s.suffix ?? ''}>{fmt(s)}</dd>
51
+ </div>
52
+ ))}
53
+ </dl>
54
+ </div>
55
+ </section>
56
+ </div>
57
+ <script>
58
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
59
+ const els = document.querySelectorAll<HTMLElement>('.scb__value[data-count]');
60
+ if (!reduce && 'IntersectionObserver' in window) {
61
+ const io = new IntersectionObserver((entries) => {
62
+ entries.forEach((entry) => {
63
+ if (!entry.isIntersecting) return;
64
+ const el = entry.target as HTMLElement;
65
+ io.unobserve(el);
66
+ const target = Number(el.dataset.count);
67
+ const dec = Number(el.dataset.decimals);
68
+ const pre = el.dataset.prefix || '';
69
+ const suf = el.dataset.suffix || '';
70
+ const final = el.textContent;
71
+ el.setAttribute('aria-label', final || '');
72
+ const start = performance.now();
73
+ const dur = 1400;
74
+ const tick = (now: number) => {
75
+ const t = Math.min(1, (now - start) / dur);
76
+ const eased = 1 - Math.pow(1 - t, 3);
77
+ const v = target * eased;
78
+ el.textContent = `${pre}${v.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec })}${suf}`;
79
+ if (t < 1) requestAnimationFrame(tick);
80
+ else el.textContent = final;
81
+ };
82
+ requestAnimationFrame(tick);
83
+ });
84
+ }, { threshold: 0.4 });
85
+ els.forEach((el) => io.observe(el));
86
+ }
87
+ </script>
88
+
89
+ <style>
90
+ .cq-stats-counter-band-cq { container-type: inline-size; }
91
+ .scb {
92
+ --scb-py: 4.5rem;
93
+ padding: var(--scb-py) var(--sp-6, 1.5rem);
94
+ background: linear-gradient(180deg, transparent, rgb(var(--ink-rgb, 255 255 255) / 0.02) 50%, transparent), var(--c-bg, #050505);
95
+ }
96
+ .scb--sm { --scb-py: 3rem; }
97
+ .scb--lg { --scb-py: 6.5rem; }
98
+ .scb__inner { max-width: 1180px; margin: 0 auto; }
99
+ .scb__title { margin: 0 0 2.5rem; text-align: center; font-size: 0.875rem; font-weight: 650; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
100
+ .scb__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; margin: 0; }
101
+ .scb__grid { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
102
+ .scb__item { display: flex; flex-direction: column-reverse; align-items: center; gap: 0.5rem; padding: 1.5rem 1rem; text-align: center; }
103
+ .scb--divided .scb__item + .scb__item { border-left: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); }
104
+ .scb--cards .scb__grid { gap: 1rem; }
105
+ .scb--cards .scb__item { border-radius: var(--r-lg, 20px); background: rgb(var(--ink-rgb, 255 255 255) / 0.03); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); }
106
+ .scb__value {
107
+ margin: 0;
108
+ font-size: clamp(2.25rem, 5vw, 3.25rem);
109
+ font-weight: 800;
110
+ letter-spacing: -0.04em;
111
+ line-height: 1;
112
+ font-variant-numeric: tabular-nums;
113
+ background: linear-gradient(180deg, var(--c-text-1, #fff), rgb(var(--ink-rgb, 255 255 255) / 0.65));
114
+ -webkit-background-clip: text;
115
+ background-clip: text;
116
+ color: transparent;
117
+ }
118
+ .scb__label { font-size: 0.9375rem; color: var(--c-text-2, #a3a3a3); max-width: 220px; }
119
+ @container (max-width: 640px) {
120
+ .scb__grid { grid-template-columns: 1fr 1fr; }
121
+ .scb--divided .scb__item + .scb__item { border-left: 0; }
122
+ .scb--divided .scb__item:nth-child(even) { border-left: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); }
123
+ }
124
+ </style>
@@ -0,0 +1,85 @@
1
+ ---
2
+ /**
3
+ * StatsDashboardSection — Complete analytics section: heading, KPI tile row and
4
+ * two charts (trend + ranked breakdown) composed from the kit's SVG charts.
5
+ *
6
+ * @param {string} eyebrow - Small label above the heading.
7
+ * @param {string} title - Section heading.
8
+ * @param {string} subtitle - Section description.
9
+ * @param {Array<{label: string, value: string, change: number, trend?: number[], invert?: boolean}>} kpis - KPI tiles.
10
+ * @param {string} variant - Chart card style (glass|solid|ghost).
11
+ * @param {string} size - Vertical rhythm (sm|md|lg).
12
+ * @param {string} className - Extra CSS class.
13
+ */
14
+ import KpiStatTile from '../charts/KpiStatTile.astro';
15
+ import StackedAreaChart from '../charts/StackedAreaChart.astro';
16
+ import HorizontalBarChart from '../charts/HorizontalBarChart.astro';
17
+
18
+ interface Kpi { label: string; value: string; change: number; trend?: number[]; invert?: boolean }
19
+ interface Props {
20
+ eyebrow?: string;
21
+ title?: string;
22
+ subtitle?: string;
23
+ kpis?: Kpi[];
24
+ variant?: 'glass' | 'solid' | 'ghost';
25
+ size?: 'sm' | 'md' | 'lg';
26
+ className?: string;
27
+ }
28
+
29
+ const {
30
+ eyebrow = 'Live metrics',
31
+ title = 'Growth you can see',
32
+ subtitle = 'Real numbers from teams running on the kit, updated every build.',
33
+ kpis = [
34
+ { label: 'Monthly visitors', value: '1.2M', change: 18.4, trend: [60, 64, 70, 72, 80, 88, 95, 104, 120] },
35
+ { label: 'Conversion rate', value: '4.6%', change: 0.9, trend: [3.4, 3.6, 3.7, 3.9, 4, 4.2, 4.3, 4.5, 4.6] },
36
+ { label: 'Avg. load time', value: '0.8s', change: -22, invert: true, trend: [1.3, 1.25, 1.1, 1.05, 1, 0.95, 0.9, 0.85, 0.8] },
37
+ { label: 'Bounce rate', value: '31%', change: -4.2, invert: true, trend: [38, 37, 36, 35, 34, 33, 33, 32, 31] },
38
+ ],
39
+ variant = 'glass',
40
+ size = 'md',
41
+ className = '',
42
+ } = Astro.props;
43
+
44
+ const id = `sds-${Math.random().toString(36).slice(2, 8)}`;
45
+ ---
46
+
47
+ <div class="cq-stats-dashboard-section-cq">
48
+ <section class={`sds sds--${size} ${className}`} aria-labelledby={id}>
49
+ <div class="sds__inner">
50
+ <header class="sds__head">
51
+ <p class="sds__eyebrow"><span class="sds__live" aria-hidden="true"></span>{eyebrow}</p>
52
+ <h2 id={id} class="sds__title">{title}</h2>
53
+ <p class="sds__subtitle">{subtitle}</p>
54
+ </header>
55
+ <div class="sds__kpis">
56
+ {kpis.map((k) => (
57
+ <KpiStatTile label={k.label} value={k.value} change={k.change} trend={k.trend} invert={k.invert} period="vs last quarter" variant={variant} size="sm" />
58
+ ))}
59
+ </div>
60
+ <div class="sds__charts">
61
+ <StackedAreaChart title="Sessions by channel" subtitle="Thousands per month" variant={variant} />
62
+ <HorizontalBarChart title="Top landing pages" subtitle="Visitors, last 30 days" variant={variant} />
63
+ </div>
64
+ </div>
65
+ </section>
66
+ </div>
67
+ <style>
68
+ .cq-stats-dashboard-section-cq { container-type: inline-size; }
69
+ .sds {
70
+ --sds-py: 5.5rem;
71
+ padding: var(--sds-py) var(--sp-6, 1.5rem);
72
+ background: radial-gradient(ellipse 60% 40% at 50% 0%, rgba(57, 135, 229, 0.08), transparent 70%), var(--c-bg, #050505);
73
+ }
74
+ .sds--sm { --sds-py: 3.5rem; }
75
+ .sds--lg { --sds-py: 7.5rem; }
76
+ .sds__inner { max-width: 1180px; margin: 0 auto; display: grid; gap: 1.25rem; }
77
+ .sds__head { text-align: center; max-width: 640px; margin: 0 auto 1.75rem; }
78
+ .sds__eyebrow { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0 0 0.75rem; font-size: 0.8125rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
79
+ .sds__live { width: 8px; height: 8px; border-radius: 50%; background: var(--chart-pos, #34c38f); box-shadow: 0 0 10px var(--chart-pos, #34c38f); }
80
+ .sds__title { margin: 0 0 0.75rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
81
+ .sds__subtitle { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
82
+ .sds__kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1.25rem; }
83
+ .sds__charts { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1.25rem; }
84
+ @container (max-width: 900px) { .sds__charts { grid-template-columns: 1fr; } }
85
+ </style>
@@ -52,6 +52,7 @@ const stats = [
52
52
  ];
53
53
  ---
54
54
 
55
+ <div class="cq-stats-metric-showcase-cq">
55
56
  <section class={`stats-section ${className}`} aria-labelledby="stats-title">
56
57
  <div class="stats-bg" aria-hidden="true">
57
58
  <div class="stats-glow"></div>
@@ -105,8 +106,9 @@ const stats = [
105
106
  </div>
106
107
  </div>
107
108
  </section>
108
-
109
+ </div>
109
110
  <style>
111
+ .cq-stats-metric-showcase-cq { container-type: inline-size; }
110
112
  .stats-section {
111
113
  position: relative;
112
114
  padding: var(--sp-20, 5rem) var(--sp-6, 1.5rem);
@@ -185,7 +187,7 @@ const stats = [
185
187
  .stat-card {
186
188
  background: rgba(15, 23, 42, 0.6);
187
189
  backdrop-filter: blur(20px);
188
- border: 1px solid rgba(255, 255, 255, 0.08);
190
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
189
191
  border-radius: var(--r-xl, 20px);
190
192
  padding: 2rem 1.75rem;
191
193
  display: flex;
@@ -196,7 +198,7 @@ const stats = [
196
198
 
197
199
  .stat-card:hover {
198
200
  transform: translateY(-4px);
199
- border-color: rgba(255, 255, 255, 0.2);
201
+ border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2);
200
202
  }
201
203
 
202
204
  .stat-top {
@@ -239,7 +241,7 @@ const stats = [
239
241
 
240
242
  .stat-chart-bar {
241
243
  height: 4px;
242
- background: rgba(255, 255, 255, 0.08);
244
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.08);
243
245
  border-radius: 9999px;
244
246
  overflow: hidden;
245
247
  }
@@ -258,7 +260,7 @@ const stats = [
258
260
  gap: 2rem;
259
261
  padding: 1.25rem 2rem;
260
262
  background: rgba(10, 15, 30, 0.8);
261
- border: 1px solid rgba(255, 255, 255, 0.08);
263
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
262
264
  border-radius: 16px;
263
265
  flex-wrap: wrap;
264
266
  }
@@ -313,13 +315,13 @@ const stats = [
313
315
  font-weight: 600;
314
316
  }
315
317
 
316
- @media (max-width: 1024px) {
318
+ @container (max-width: 1024px) {
317
319
  .stats-grid {
318
320
  grid-template-columns: repeat(2, 1fr);
319
321
  }
320
322
  }
321
323
 
322
- @media (max-width: 640px) {
324
+ @container (max-width: 640px) {
323
325
  .stats-grid {
324
326
  grid-template-columns: 1fr;
325
327
  }
@@ -60,6 +60,7 @@ const members = [
60
60
  ];
61
61
  ---
62
62
 
63
+ <div class="cq-team-grid-section-cq">
63
64
  <section class={`team-section ${className}`} aria-labelledby="team-title">
64
65
  <div class="team-bg" aria-hidden="true">
65
66
  <div class="team-glow"></div>
@@ -106,8 +107,9 @@ const members = [
106
107
  </div>
107
108
  </div>
108
109
  </section>
109
-
110
+ </div>
110
111
  <style>
112
+ .cq-team-grid-section-cq { container-type: inline-size; }
111
113
  .team-section {
112
114
  position: relative;
113
115
  padding: var(--sp-20, 5rem) var(--sp-6, 1.5rem);
@@ -186,7 +188,7 @@ const members = [
186
188
  .member-card {
187
189
  background: rgba(15, 23, 42, 0.55);
188
190
  backdrop-filter: blur(20px);
189
- border: 1px solid rgba(255, 255, 255, 0.08);
191
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
190
192
  border-radius: var(--r-xl, 20px);
191
193
  padding: 2rem 1.5rem;
192
194
  text-align: center;
@@ -198,7 +200,7 @@ const members = [
198
200
 
199
201
  .member-card:hover {
200
202
  transform: translateY(-4px);
201
- border-color: rgba(255, 255, 255, 0.2);
203
+ border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2);
202
204
  }
203
205
 
204
206
  .member-avatar {
@@ -211,7 +213,7 @@ const members = [
211
213
  font-weight: 800;
212
214
  color: #fff;
213
215
  margin-bottom: 1.25rem;
214
- border: 2px solid rgba(255, 255, 255, 0.2);
216
+ border: 2px solid rgb(var(--ink-rgb, 255 255 255) / 0.2);
215
217
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
216
218
  }
217
219
 
@@ -246,8 +248,8 @@ const members = [
246
248
  width: 34px;
247
249
  height: 34px;
248
250
  border-radius: 8px;
249
- background: rgba(255, 255, 255, 0.05);
250
- border: 1px solid rgba(255, 255, 255, 0.08);
251
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.05);
252
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
251
253
  display: flex;
252
254
  align-items: center;
253
255
  justify-content: center;
@@ -258,8 +260,8 @@ const members = [
258
260
 
259
261
  .social-icon:hover {
260
262
  color: #fff;
261
- background: rgba(255, 255, 255, 0.12);
262
- border-color: rgba(255, 255, 255, 0.2);
263
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.12);
264
+ border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2);
263
265
  }
264
266
 
265
267
  /* Hiring Banner */
@@ -314,13 +316,13 @@ const members = [
314
316
  box-shadow: 0 6px 20px rgba(168, 85, 247, 0.45);
315
317
  }
316
318
 
317
- @media (max-width: 1024px) {
319
+ @container (max-width: 1024px) {
318
320
  .team-grid {
319
321
  grid-template-columns: repeat(2, 1fr);
320
322
  }
321
323
  }
322
324
 
323
- @media (max-width: 640px) {
325
+ @container (max-width: 640px) {
324
326
  .team-grid {
325
327
  grid-template-columns: 1fr;
326
328
  }
@@ -0,0 +1,167 @@
1
+ ---
2
+ /**
3
+ * TestimonialCarousel — Scroll-snap testimonial carousel with previous/next
4
+ * buttons, keyboard support and no autoplay (respects user control).
5
+ *
6
+ * @param {string} title - Section heading.
7
+ * @param {Array<{quote: string, name: string, role: string, rating?: number}>} items - Testimonials.
8
+ * @param {string} variant - Card style (glass|solid|quote).
9
+ * @param {string} size - Vertical rhythm (sm|md|lg).
10
+ * @param {string} className - Extra CSS class.
11
+ */
12
+ interface Item { quote: string; name: string; role: string; rating?: number }
13
+ interface Props {
14
+ title?: string;
15
+ items?: Item[];
16
+ variant?: 'glass' | 'solid' | 'quote';
17
+ size?: 'sm' | 'md' | 'lg';
18
+ className?: string;
19
+ }
20
+
21
+ const {
22
+ title = 'Loved by builders worldwide',
23
+ items = [
24
+ { quote: 'We rebuilt our marketing site in two days. The sections are gorgeous and Lighthouse went straight to 100.', name: 'Alicia Gómez', role: 'Head of Growth, Northwind', rating: 5 },
25
+ { quote: 'The SVG charts replaced a 200 kB library in our dashboard. Same features, zero runtime.', name: 'Marcus Lee', role: 'Staff Engineer, Lumen', rating: 5 },
26
+ { quote: 'Finally a component kit that takes accessibility seriously. Our audit came back clean.', name: 'Priya Natarajan', role: 'Accessibility Lead, Civic', rating: 5 },
27
+ { quote: 'Copy, paste, tweak tokens, done. It feels like a design system we built ourselves.', name: 'Tom Becker', role: 'Founder, Driftly', rating: 4 },
28
+ { quote: 'Our agency ships every client landing page on top of this kit now.', name: 'Sofía Ruiz', role: 'Creative Director, Studio Alba', rating: 5 },
29
+ ],
30
+ variant = 'glass',
31
+ size = 'md',
32
+ className = '',
33
+ } = Astro.props;
34
+
35
+ const id = `tc-${Math.random().toString(36).slice(2, 8)}`;
36
+ const initials = (n: string) => n.split(' ').map((p) => p[0]).slice(0, 2).join('');
37
+ const hues = ['var(--chart-1, #3987e5)', 'var(--chart-2, #d95926)', 'var(--chart-3, #199e70)', 'var(--chart-5, #d55181)', 'var(--chart-7, #9085e9)'];
38
+ ---
39
+
40
+ <div class="cq-testimonial-carousel-cq">
41
+ <section class={`tc tc--${variant} tc--${size} ${className}`} aria-labelledby={id} aria-roledescription="carousel" data-tc>
42
+ <div class="tc__inner">
43
+ <header class="tc__head">
44
+ <h2 id={id} class="tc__title">{title}</h2>
45
+ <div class="tc__controls">
46
+ <button type="button" class="tc__btn" data-dir="-1" aria-label="Previous testimonials">
47
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
48
+ </button>
49
+ <button type="button" class="tc__btn" data-dir="1" aria-label="Next testimonials">
50
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M9 18l6-6-6-6" /></svg>
51
+ </button>
52
+ </div>
53
+ </header>
54
+
55
+ <ul class="tc__track" tabindex="0" aria-label="Testimonials, scroll horizontally">
56
+ {items.map((t, i) => (
57
+ <li class="tc__slide" aria-roledescription="slide" aria-label={`${i + 1} of ${items.length}`}>
58
+ <figure class="tc__card">
59
+ {t.rating && (
60
+ <div class="tc__stars" role="img" aria-label={`${t.rating} out of 5 stars`}>
61
+ {Array.from({ length: 5 }, (_, s) => (
62
+ <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" class={(t.rating ?? 0) > s ? 'is-on' : ''}><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z" fill="currentColor" /></svg>
63
+ ))}
64
+ </div>
65
+ )}
66
+ <blockquote class="tc__quote"><p>“{t.quote}”</p></blockquote>
67
+ <figcaption class="tc__author">
68
+ <span class="tc__avatar" style={`background:${hues[i % hues.length]}`} aria-hidden="true">{initials(t.name)}</span>
69
+ <span><strong>{t.name}</strong><span class="tc__role">{t.role}</span></span>
70
+ </figcaption>
71
+ </figure>
72
+ </li>
73
+ ))}
74
+ </ul>
75
+ </div>
76
+ </section>
77
+ </div>
78
+ <script>
79
+ document.querySelectorAll<HTMLElement>('[data-tc]').forEach((root) => {
80
+ const track = root.querySelector<HTMLElement>('.tc__track');
81
+ const btns = root.querySelectorAll<HTMLButtonElement>('.tc__btn');
82
+ if (!track) return;
83
+ const update = () => {
84
+ const max = track.scrollWidth - track.clientWidth - 2;
85
+ btns[0].disabled = track.scrollLeft <= 2;
86
+ btns[1].disabled = track.scrollLeft >= max;
87
+ };
88
+ const step = () => (track.querySelector<HTMLElement>('.tc__slide')?.offsetWidth ?? 300) + 20;
89
+ btns.forEach((b) => b.addEventListener('click', () => {
90
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
91
+ track.scrollBy({ left: Number(b.dataset.dir) * step(), behavior: reduce ? 'auto' : 'smooth' });
92
+ }));
93
+ track.addEventListener('scroll', update, { passive: true });
94
+ window.addEventListener('resize', update);
95
+ update();
96
+ });
97
+ </script>
98
+
99
+ <style>
100
+ .cq-testimonial-carousel-cq { container-type: inline-size; }
101
+ .tc {
102
+ --tc-py: 5.5rem;
103
+ padding: var(--tc-py) var(--sp-6, 1.5rem);
104
+ background: var(--c-bg, #050505);
105
+ overflow: hidden;
106
+ }
107
+ .tc--sm { --tc-py: 3.5rem; }
108
+ .tc--lg { --tc-py: 7.5rem; }
109
+ .tc__inner { max-width: 1180px; margin: 0 auto; }
110
+ .tc__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1.5rem; margin-bottom: 2rem; }
111
+ .tc__title { margin: 0; font-size: clamp(1.75rem, 4vw, 2.5rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); max-width: 560px; }
112
+ .tc__controls { display: flex; gap: 0.5rem; flex-shrink: 0; }
113
+ .tc__btn {
114
+ display: grid;
115
+ place-items: center;
116
+ width: 48px;
117
+ height: 48px;
118
+ border-radius: 50%;
119
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14);
120
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
121
+ color: var(--c-text-1, #fff);
122
+ cursor: pointer;
123
+ transition: background 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
124
+ }
125
+ .tc__btn:hover:not(:disabled) { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.3); }
126
+ .tc__btn:disabled { opacity: 0.35; cursor: not-allowed; }
127
+ .tc__btn:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
128
+ .tc__track {
129
+ list-style: none;
130
+ margin: 0;
131
+ padding: 0.25rem 0 1rem;
132
+ display: grid;
133
+ grid-auto-flow: column;
134
+ grid-auto-columns: min(380px, 85%);
135
+ gap: 20px;
136
+ overflow-x: auto;
137
+ scroll-snap-type: x mandatory;
138
+ scrollbar-width: none;
139
+ }
140
+ .tc__track::-webkit-scrollbar { display: none; }
141
+ .tc__track:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 4px; border-radius: 12px; }
142
+ .tc__slide { scroll-snap-align: start; }
143
+ .tc__card {
144
+ height: 100%;
145
+ margin: 0;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 1.25rem;
149
+ padding: 1.75rem;
150
+ border-radius: var(--r-lg, 20px);
151
+ box-sizing: border-box;
152
+ }
153
+ .tc--glass .tc__card { background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.05), rgb(var(--ink-rgb, 255 255 255) / 0.015)); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); }
154
+ .tc--solid .tc__card { background: var(--c-bg-2, #111); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.05); }
155
+ .tc--quote .tc__card { border-left: 2px solid var(--c-primary-light, #d4a017); border-radius: 0; padding-left: 1.5rem; }
156
+ .tc__stars { display: flex; gap: 2px; color: rgb(var(--ink-rgb, 255 255 255) / 0.15); }
157
+ .tc__stars .is-on { color: var(--c-primary-light, #d4a017); }
158
+ .tc__quote { margin: 0; flex: 1; }
159
+ .tc__quote p { margin: 0; font-size: 1.0625rem; line-height: 1.6; color: var(--c-text-1, #fff); }
160
+ .tc__author { display: flex; align-items: center; gap: 0.75rem; font-size: 0.875rem; }
161
+ .tc__author strong { display: block; color: var(--c-text-1, #fff); font-weight: 650; }
162
+ .tc__role { display: block; color: var(--c-text-2, #a3a3a3); font-size: 0.8125rem; }
163
+ .tc__avatar { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; color: #fff; font-weight: 700; font-size: 0.8125rem; flex-shrink: 0; }
164
+ @container (max-width: 560px) {
165
+ .tc__head { flex-direction: column; align-items: flex-start; }
166
+ }
167
+ </style>