diff --git a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html
index 6aedae5..b58120c 100644
--- a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html
+++ b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html
@@ -113,7 +113,14 @@
Price for {{ price.product }}
@@ -161,7 +168,13 @@ Pricing
- | Flat rate |
+
+ Flat rate
+
+
+ |
@@ -202,12 +215,24 @@ Currencies
Upsells
-
Boosts revenue
+
+
+
Boosts revenue
+

+
+
diff --git a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts
index c6f01e9..03d8e4b 100644
--- a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts
+++ b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts
@@ -13,6 +13,7 @@ import { PriceActionsHostComponent } from '../../components/price-actions-host/p
import { ActivatedRoute, Router } from '@angular/router';
import { PopupMenuAction, PopupMenuComponent } from '../../../../../shared';
import { EventsListComponent } from '../../../components';
+import { MoreInfoHoverComponent } from '../../../../../shared';
import { Subscription } from 'rxjs';
@@ -24,6 +25,7 @@ import { Subscription } from 'rxjs';
DecimalPipe,
UpperCasePipe,
EventsListComponent,
+ MoreInfoHoverComponent,
],
templateUrl: './price-detail.component.html',
styleUrl: './price-detail.component.scss',
diff --git a/apps/web/src/app/shared/ui/index.ts b/apps/web/src/app/shared/ui/index.ts
index e5264c6..3b09b90 100644
--- a/apps/web/src/app/shared/ui/index.ts
+++ b/apps/web/src/app/shared/ui/index.ts
@@ -9,3 +9,4 @@ export * from './status-chip/status-chip.component';
export * from './test-mode-banner/test-mode-banner.component';
export * from './confirm-dialog/confirm-dialog.component';
export * from './popup-menu/popup-menu.component';
+export * from './more-info-hover/more-info-hover.component';
diff --git a/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.html b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.html
new file mode 100644
index 0000000..18e21ae
--- /dev/null
+++ b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ @if (text) {
+ {{ text }}
+ } @if (linkUrl) {
+ {{ linkText }}
+ }
+
+
+
diff --git a/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.scss b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.scss
new file mode 100644
index 0000000..173d7dd
--- /dev/null
+++ b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.scss
@@ -0,0 +1,130 @@
+@use '../../../styles/base.scss' as *;
+
+$bubble-gap: $spacing-small;
+$bubble-tail-size: 7px;
+$bubble-tail-width: 10px;
+
+:host {
+ display: inline-flex;
+ align-items: center;
+ line-height: 0;
+}
+
+.more-info-wrapper {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ line-height: 0;
+ outline: none;
+}
+
+.more-info-default-icon {
+ transition: opacity $transition;
+}
+
+.more-info-wrapper:hover .more-info-default-icon,
+.more-info-wrapper:focus-visible .more-info-default-icon {
+ opacity: 1;
+}
+
+.more-info-bubble {
+ position: absolute;
+ left: 50%;
+ z-index: 30;
+ min-width: 280px;
+ max-width: 360px;
+ padding: $spacing-snug $spacing;
+ background-color: $background-color;
+ border: 1px solid $darkest-background-color;
+ border-radius: $border-radius-small;
+ font-size: $font-size;
+ line-height: 1.45;
+ color: $text-color;
+ text-align: left;
+ white-space: normal;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity $transition-fast ease-out,
+ transform $transition-fast ease-out, visibility 0s linear $transition-fast;
+
+ a {
+ display: inline-block;
+ margin-top: $spacing-extra-small;
+ font-size: $font-size;
+ }
+
+ span + a {
+ margin-left: $spacing-extra-small;
+ }
+
+ // Transparent bridge across the visual gap so the cursor stays inside the
+ // wrapper while moving from the trigger to the bubble (and back).
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: $bubble-gap;
+ }
+
+ // The tail/pointer. A CSS triangle drawn with borders; drop-shadow paints
+ // the matching outline so it lines up with the bubble border.
+ &::after {
+ content: '';
+ position: absolute;
+ left: 50%;
+ width: 0;
+ height: 0;
+ margin-left: -#{$bubble-tail-width};
+ border-left: $bubble-tail-width solid transparent;
+ border-right: $bubble-tail-width solid transparent;
+ }
+}
+
+.more-info-bubble-bottom {
+ top: 100%;
+ transform: translateX(-50%) translateY(calc(#{$bubble-gap} + 4px));
+
+ &::before {
+ top: -$bubble-gap;
+ }
+
+ &::after {
+ top: -$bubble-tail-size;
+ border-bottom: $bubble-tail-size solid $background-color;
+ filter: drop-shadow(0 -1px 0 $darkest-background-color);
+ }
+}
+
+.more-info-bubble-top {
+ bottom: 100%;
+ transform: translateX(-50%) translateY(calc(-1 * (#{$bubble-gap} + 4px)));
+
+ &::before {
+ bottom: -$bubble-gap;
+ }
+
+ &::after {
+ bottom: -$bubble-tail-size;
+ border-top: $bubble-tail-size solid $background-color;
+ filter: drop-shadow(0 1px 0 $darkest-background-color);
+ }
+}
+
+.more-info-wrapper:hover .more-info-bubble,
+.more-info-wrapper:focus-within .more-info-bubble {
+ opacity: 1;
+ visibility: visible;
+ transition: opacity $transition-fast ease-out,
+ transform $transition-fast ease-out, visibility 0s linear 0s;
+}
+
+.more-info-wrapper:hover .more-info-bubble-bottom,
+.more-info-wrapper:focus-within .more-info-bubble-bottom {
+ transform: translateX(-50%) translateY($bubble-gap);
+}
+
+.more-info-wrapper:hover .more-info-bubble-top,
+.more-info-wrapper:focus-within .more-info-bubble-top {
+ transform: translateX(-50%) translateY(calc(-1 * #{$bubble-gap}));
+}
diff --git a/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.ts b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.ts
new file mode 100644
index 0000000..eae483d
--- /dev/null
+++ b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.ts
@@ -0,0 +1,21 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-more-info-hover',
+ imports: [],
+ templateUrl: './more-info-hover.component.html',
+ styleUrl: './more-info-hover.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MoreInfoHoverComponent {
+ /** Body text shown inside the hover bubble. Ignored if a [bubble] slot is projected. */
+ @Input() text = '';
+ /** Optional link label shown after the body text. Defaults to "Learn more" when a url is given. */
+ @Input() linkText = 'Learn more';
+ /** Optional url for the link. */
+ @Input() linkUrl = '';
+ /** Accessible label applied to the default info icon trigger. */
+ @Input() ariaLabel = 'More info';
+ /** Where the bubble appears relative to the trigger. */
+ @Input() position: 'top' | 'bottom' = 'top';
+}