/* ── Content (variant-shared base) ──────────────────────────────────────── */ [data-floating-wrapper]:has(> [data-tooltip-content]) { z-index: var(--tooltip-content-z); } [data-tooltip-content] { --_tooltip-py: var(--tooltip-padding-y-md); --_tooltip-px: var(--tooltip-padding-x-md); --_tooltip-font-size: var(--tooltip-font-size-md); --_tooltip-bg: var(--tooltip-bg); --_tooltip-border: var(--tooltip-border); --_tooltip-shadow: var(--tooltip-shadow); --_tooltip-width: var(--_tooltip-content-width-override, var(--_tooltip-match-anchor-width, auto)); --_tooltip-max-width: var(--_tooltip-content-max-width-override, var(--tooltip-max-width)); --_tooltip-max-height: var( --_tooltip-content-max-height-override, var(--floating-available-height, calc(100dvh - var(--space-4))) ); display: block; box-sizing: border-box; inline-size: var(--_tooltip-width); min-inline-size: var(--_tooltip-content-min-width-override, auto); max-inline-size: min(var(--_tooltip-max-width), calc(100vw - var(--space-4))); block-size: var(--_tooltip-content-height-override, auto); min-block-size: var(--_tooltip-content-min-height-override, auto); max-block-size: var(--_tooltip-max-height); padding: var(--_tooltip-py) var(--_tooltip-px); /* `visible` so the absolutely-positioned caret (containing block = this relative * content) is NOT clipped where it pokes past the panel edge — the same reason * the popover content is `overflow: visible`. Without this the arrow was clipped * away entirely. Tooltips are short, so no inner scroll viewport is needed. */ overflow: visible; border: var(--tooltip-border-width) solid var(--_tooltip-border); border-radius: var(--tooltip-radius); background: var(--_tooltip-bg); color: var(--tooltip-color); box-shadow: var(--_tooltip-shadow); transform-origin: var(--floating-transform-origin, center); font-family: var(--tooltip-font-family); font-size: var(--_tooltip-font-size); font-weight: var(--tooltip-font-weight); line-height: var(--tooltip-line-height); text-wrap: pretty; } /* ── Sizes ──────────────────────────────────────────────────────────────── */ [data-tooltip-content][data-size='sm'] { --_tooltip-py: var(--tooltip-padding-y-sm); --_tooltip-px: var(--tooltip-padding-x-sm); --_tooltip-font-size: var(--tooltip-font-size-sm); } [data-tooltip-content][data-size='lg'] { --_tooltip-py: var(--tooltip-padding-y-lg); --_tooltip-px: var(--tooltip-padding-x-lg); --_tooltip-font-size: var(--tooltip-font-size-lg); } /* ── Variants ───────────────────────────────────────────────────────────── */ /* `solid` is the default surface — the base block already paints it. */ [data-tooltip-content][data-variant='outline'] { --_tooltip-bg: transparent; --_tooltip-border: var(--color-border-strong); --_tooltip-shadow: none; } [data-tooltip-content][data-variant='ghost'] { --_tooltip-bg: color-mix(in srgb, var(--color-surface-overlay) 70%, transparent); --_tooltip-border: transparent; --_tooltip-shadow: none; backdrop-filter: blur(var(--blur-md)); } /* ── Arrow ──────────────────────────────────────────────────────────────── */ /* `display: block` keeps the SVG off the text baseline — inline added ~21px of * phantom height that both misaligned the caret AND pushed the panel too far from * the trigger (the positioner adds the measured arrow height to the gap). */ [data-tooltip-arrow] svg { display: block; overflow: visible; } /* Colour the SVG shapes directly — the polygon/path carry `fill`/`stroke = * "currentColor"` presentation attributes that a `fill`/`stroke` on the wrapping * can't override. The body matches the panel bg (a seamless caret, not a * dark blob) and the two exposed edges take the panel border. Now that the content * is `overflow: visible` (the caret is no longer clipped), that border outline is * what makes the matched caret read against the page — same as the popover. */ [data-tooltip-arrow] polygon { fill: var(--_tooltip-bg, var(--tooltip-bg)); stroke: none; } [data-tooltip-arrow] path { stroke: var(--_tooltip-border, var(--tooltip-border)); stroke-width: var(--tooltip-border-width); vector-effect: non-scaling-stroke; } /* Pull the caret base into the panel by the border width (per side) so the fill * covers the panel's border there and the side strokes continue the outline — * otherwise the panel border draws a line across the caret base. Mirrors the * popover recipe. */ [data-tooltip-arrow][data-side='bottom'] { margin-block-start: var(--tooltip-border-width); } [data-tooltip-arrow][data-side='top'] { margin-block-end: var(--tooltip-border-width); } [data-tooltip-arrow][data-side='left'] { margin-inline-end: var(--tooltip-border-width); } [data-tooltip-arrow][data-side='right'] { margin-inline-start: var(--tooltip-border-width); } /* Outline variant: transparent bg → fill with the surface, stroke the edges. */ [data-tooltip-content][data-variant='outline'] ~ [data-tooltip-arrow] polygon, [data-tooltip-content][data-variant='outline'] [data-tooltip-arrow] polygon { fill: var(--color-surface-default); } [data-tooltip-content][data-variant='outline'] ~ [data-tooltip-arrow] path, [data-tooltip-content][data-variant='outline'] [data-tooltip-arrow] path { stroke: var(--color-border-strong); } /* ── Entrance / exit ───────────────────────────────────────────────────────── * Tooltip morfo's data-state values: 'closed' | 'delayed-open' | 'instant-open'. * 'delayed-open' (hover) scales in subtly; 'instant-open' (keyboard focus) * appears with no entrance; 'closed' scales out. The scale grows from * `transform-origin` (the floating origin, toward the anchor — set above), so it * keeps a directional feel without per-side keyframes. The tooltip's 3-value * state (delayed-open, not the generic open/closed) is why this is a bespoke * recipe keyframe, not a registered `motion` preset. */ [data-tooltip-content][data-state='delayed-open'] { animation: eidos-tooltip-enter var(--duration-normal) var(--ease-out) both; } [data-tooltip-content][data-state='closed'] { animation: eidos-tooltip-exit var(--duration-fast) var(--ease-in) both; } /* instant-open: no entrance — element appears at final position. */ [data-tooltip-content][data-state='instant-open'] { opacity: 1; transform: none; } /* Subtle entrance — a gentle scale + fade from the floating origin, no * overshoot/bounce. `scale` (the individual property, not the `transform` * shorthand) composes with `transform-origin` and never clobbers a sibling * transform. */ @keyframes eidos-tooltip-enter { from { opacity: 0; scale: 0.96; } to { opacity: 1; scale: 1; } } @keyframes eidos-tooltip-exit { to { opacity: 0; scale: 0.97; } } /* ── Trigger states ───────────────────────────────────────────────────────── * Trigger is the wrapper around any focusable element (button, link, …). * The audit (R-1.2 / R-1.5) requires the recipe to acknowledge `data-disabled` * and `:focus-visible`. The wrapped element keeps its own styling; the rules * here suppress the tooltip behavior cleanly when the trigger is disabled. */ [data-tooltip-trigger][data-disabled] { pointer-events: none; cursor: not-allowed; } [data-tooltip-trigger]:focus-visible { /* Defer the focus ring to the wrapped element — but ensure the outline is at least visible if the trigger is rendered as a focusable shell. */ outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); } /* ── Reduced motion ───────────────────────────────────────────────────────── * Honour the user's OS-level preference: tooltips appear instantly with * no slide-in / slide-out, just a binary visibility toggle. The exit * animation is also short-circuited so the element is removed without a * trailing fade. */ @media (prefers-reduced-motion: reduce) { [data-tooltip-content][data-state='delayed-open'], [data-tooltip-content][data-state='instant-open'], [data-tooltip-content][data-state='closed'] { animation: none; } }