/* ============================================================
   components/tooltip.css — .dd-tooltip
   Tooltip shown on hover + keyboard focus. Visual text comes
   from data-tip; for screen readers ALSO give the element an
   aria-label (the bubble is not announced).

   Two bubbles, one at a time. With `js/components.js` running, every
   tooltip is the shared `.dd-tip` at the bottom of this file, placed
   where it fits (#265). The `::after` bubble in between is the fallback
   for a page the script never ran on, and its placement modifiers are
   the script's preferences.
     <button class="dd-tooltip" data-tip="copy link" aria-label="copy link">…</button>

   Where the two would say the same words, drop the data-tip and the bubble
   reads the aria-label instead (#208):
     <button class="dd-tooltip" aria-label="artist actions">…</button>

   And an icon-only button needs neither the class nor the attribute — it
   cannot speak for itself, it already carries an aria-label, so it gets the
   tooltip on sight. Both rules are below `.dd-tooltip::before`.
   ============================================================ */

/* An icon-only button is the one control that cannot say what it does. It has
   always carried the answer — every `.dd-btn--icon` in the tree has an
   `aria-label`, because a button with no text has to — but only a screen
   reader was ever told. So the label is the tooltip, and the pointer user is
   told the same thing as everybody else.

   Derived rather than restated: an `aria-label` + a `data-tip` saying the same
   words is one string to translate twice and two places for it to drift, at
   49 call sites. Nothing is added to the markup, and nothing new is
   translated — the label was already both.

   The added selector is wrapped in `:where()` so it weighs **nothing**. That is
   load-bearing: `.dd-btn--icon[aria-label]` would otherwise outweigh
   `.dd-tooltip--right` / `--below` / `--wrap`, and an icon button asking for a
   placement would silently keep the default one.

   To say something *other* than the label, add `.dd-tooltip` + `data-tip` as
   any other element does — the `:not([data-tip])` on the content rule stands
   back for it. */

.dd-tooltip,
:where(.dd-btn--icon[aria-label]) {
  position: relative;
}

.dd-tooltip::after,
:where(.dd-btn--icon[aria-label])::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: var(--space-2) var(--space-3);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  line-height: 1.3;
  white-space: nowrap;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* `visibility` is in the list so the bubble can be *told* when to appear:
     a 0s transition still honours a delay, so the hover rule below moves both
     it and the opacity to the far side of `--dd-tip-delay`. No delay here, so
     leaving takes it away at once — the wait is for arriving, not for going. */
  transition: opacity var(--dur-fast) var(--ease-soft),
              transform var(--dur-fast) var(--ease-soft),
              visibility 0s;
  z-index: var(--z-overlay);
}

/* No `data-tip` means "say the accessible name" (#208). Both halves of the
   pair are already required of a control with no words on it, and saying them
   twice is one string translated twice with two places for it to drift — so
   the second one is now optional, and a control that says the same thing to
   everybody restates nothing:

     <button class="dd-tooltip" aria-label="artist actions">⋯</button>

   Higher than `.dd-tooltip::after` on purpose: `attr()` on a missing attribute
   is not "no rule", it is an empty bubble, so this has to outrank the
   `data-tip` default rather than merely follow it. */
.dd-tooltip:not([data-tip])[aria-label]::after,
.dd-btn--icon[aria-label]:not([data-tip])::after {
  content: attr(aria-label);
}

/* little arrow */
.dd-tooltip::before,
:where(.dd-btn--icon[aria-label])::before {
  content: "";
  position: absolute;
  bottom: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: var(--ink);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-fast) var(--ease-soft),
              visibility 0s;
  z-index: var(--z-overlay);
}

/* Hovering asks a question, and the answer waits a beat before arriving: a
   pointer crossing a row of buttons on its way somewhere else is not asking
   anything, and eight bubbles firing on the way past is the thing that makes
   people stop reading them. `--dd-tip-delay` is that beat, and a call site can
   shorten it where the control is hard to reach by accident.

   **Keyboard focus does not wait.** Somebody who has tabbed onto a control has
   asked as plainly as it is possible to ask.

   `:has(:focus-visible)` rather than `:focus-within`, and that is the fix for
   a bubble that would not go away: `:focus-within` matches any focus at all,
   including the focus a *mouse click* leaves behind, so a clicked tab kept its
   tooltip open long after the pointer had gone. `:focus-visible` is the same
   question asked properly — it is the browser's own answer to "did this focus
   come from the keyboard" — and `:has()` carries it to a wrapper standing in
   for the control inside it. */
.dd-tooltip:hover::after,
:where(.dd-btn--icon[aria-label]):hover::after {
  transition-delay: var(--dd-tip-delay, 1s);
}
.dd-tooltip:hover::before,
:where(.dd-btn--icon[aria-label]):hover::before {
  transition-delay: var(--dd-tip-delay, 1s);
}

.dd-tooltip:hover::after,
.dd-tooltip:focus-visible::after,
.dd-tooltip:has(:focus-visible)::after,
:where(.dd-btn--icon[aria-label]):hover::after,
:where(.dd-btn--icon[aria-label]):focus-visible::after {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%);
}
.dd-tooltip:hover::before,
.dd-tooltip:focus-visible::before,
.dd-tooltip:has(:focus-visible)::before,
:where(.dd-btn--icon[aria-label]):hover::before,
:where(.dd-btn--icon[aria-label]):focus-visible::before {
  opacity: 1;
  visibility: visible;
}

/* The bubble is `::after` and the arrow is `::before`, so a component that
   already draws with either has nowhere to put a tooltip — and the one that
   loses is whichever rule the cascade sees last, silently. `--no-arrow` gives
   back one of the two: the component keeps `::before` for its own mark, the
   tooltip keeps `::after` for the words, and nobody is guessing which won.

   Worth it where the alternative is a wrapper the pointer has to find: a tab's
   hover target is the whole tab, and `:hover` does not reach *down* into a
   span, so a wrapper there would only answer over the label text.
   `NothingHostsTwoPseudoElements` in the suite is what finds these. */
.dd-tooltip--no-arrow::before {
  content: none;
}

/* A form control paints no pseudo-element of its own — `<select>`, `<input>`
   and `<textarea>` are replaced elements, and `::after` on one is dropped on
   the floor with nothing thrown. So its tooltip goes on a wrapper, and this is
   that wrapper (#208):

     <span class="dd-tooltip dd-tooltip--field" data-tip="…"><select …></span>

   `display: contents` would be the tidy answer and is the wrong one: it leaves
   no box for the bubble to be positioned against, so the tooltip would hang
   off whatever ancestor happened to be positioned. An inline-flex box that
   passes its own sizing through to the control keeps the layout the control
   already had — including inside a flex row, where the wrapper becomes the
   item and has to grow the way the control did. */
.dd-tooltip--field {
  display: inline-flex;
  min-width: 0;
  flex: 0 1 auto;
}

.dd-tooltip--field > select,
.dd-tooltip--field > input,
.dd-tooltip--field > textarea {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
}

/* A sentence rather than a label. The default is `nowrap` because a tooltip is
   normally two or three words and wrapping those reads as a mistake; an
   explanation is the other case, and it needs both permission to wrap and a
   width to wrap inside. `max-content` keeps a short one short — the max-width
   is a ceiling, not a size. */
.dd-tooltip--wrap {
  /* Roughly how far into the viewport the trigger sits, plus the margin to
     leave past the bubble. It is the one thing the kit cannot know — it is a
     fact about the call site's own layout — so it is a property the call site
     can set. The default suits a trigger a third of the way across a bar. */
  --dd-tip-inset: 20rem;
}

.dd-tooltip--wrap::after {
  white-space: normal;
  width: max-content;
  /* `max-content` up to the cap, then whatever room is actually left — so a
     window too narrow for the full width makes the bubble **taller**, wrapping
     into more lines, rather than making it hang off the edge. */
  max-width: min(28rem, calc(100vw - var(--dd-tip-inset)));
  text-align: left;
}

/* Place below instead of above. */
.dd-tooltip--below::after {
  bottom: auto;
  top: calc(100% + 10px);
  transform: translateX(-50%) translateY(-4px);
}
.dd-tooltip--below:hover::after,
.dd-tooltip--below:focus-visible::after,
.dd-tooltip--below:has(:focus-visible)::after {
  transform: translateX(-50%);
}
.dd-tooltip--below::before {
  bottom: auto;
  top: calc(100% + 4px);
  border-top-color: transparent;
  border-bottom-color: var(--ink);
}

/* Beside the trigger rather than over or under it, opening rightwards — for a
   bubble too big to sit above a control without covering what the control is
   about, and for a trigger far enough left that there is more room out to the
   side than there is anywhere else.

   **Its top is pinned, not its middle**, so a narrow window makes it grow
   downwards into the space below rather than upwards into whatever is above.
   The offset lifts the first line level with the trigger instead of level with
   the bubble's padding.

   Declared after `--below` so it wins the shared `transform`; the two do not
   combine, because this one is not a vertical placement. */
.dd-tooltip--right::after {
  left: calc(100% + 10px);
  right: auto;
  top: calc(-1 * var(--space-2));
  bottom: auto;
  transform: translateX(4px);
}
.dd-tooltip--right:hover::after,
.dd-tooltip--right:focus-visible::after,
.dd-tooltip--right:has(:focus-visible)::after {
  transform: translateX(0);
}

/* The arrow turns to match: it is the right border that shows, so its apex
   points back left at the trigger. */
.dd-tooltip--right::before {
  left: calc(100% - 2px);
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
  border-top-color: transparent;
  border-right-color: var(--ink);
}

/* Aligned to the trigger's leading edge instead of centred on it. `--wrap`
   answers the right-hand edge — the bubble narrows and grows taller — but it
   cannot answer the left one, because a centred bubble on a trigger near the
   start of the row is already half over the edge before it is measured. The
   first tab in a strip is exactly that: 95px in, with a three-line bubble.

   Declared after the placements above so it wins the shared `transform`; it
   composes with `--below`, which is the vertical half of the same question. */
.dd-tooltip--start::after {
  left: 0;
  transform: translateX(0) translateY(4px);
}
.dd-tooltip--start:hover::after,
.dd-tooltip--start:focus-visible::after,
.dd-tooltip--start:has(:focus-visible)::after {
  transform: translateX(0);
}
.dd-tooltip--start::before {
  left: 1.5em;
}

@media (prefers-reduced-motion: reduce) {
  .dd-tooltip--start::after { transform: translateX(0); }
}

/* `--start`'s mirror: pinned to the trigger's trailing edge, for a control at
   the right-hand end of its row. The lyrics panel's buttons are right-aligned
   and its fullscreen toggle sits in the bottom-right corner, so a centred
   bubble on either hangs off the side of the panel. */
.dd-tooltip--end::after {
  left: auto;
  right: 0;
  transform: translateX(0) translateY(4px);
}
.dd-tooltip--end:hover::after,
.dd-tooltip--end:focus-visible::after,
.dd-tooltip--end:has(:focus-visible)::after {
  transform: translateX(0);
}
.dd-tooltip--end::before {
  left: auto;
  right: 1.5em;
}

@media (prefers-reduced-motion: reduce) {
  .dd-tooltip--end::after { transform: translateX(0); }
}

/* off the scale: the number below is measured, not picked from the scale.

   The last resort, and only that (owner, 2026-09-01: below it "only if the
   screen is really small"). Everywhere there is room the bubble shrinks and
   grows taller instead — that is what `--dd-tip-inset` buys. Below `35rem`
   there is no room left to shrink into: what is beside the trigger is under
   `15rem` once the label before it is paid for, which is a column about four
   words wide and twelve lines tall. So it stops being a bubble and becomes a
   strip along the bottom of the viewport — always on screen, always the same
   shape — and the arrow goes with the anchoring, because there is nothing left
   for it to point at.

   The number is measured rather than guessed, against **both** languages: the
   Spanish label is 90px longer than the English one, which moves the trigger
   and with it every width that fits. Guessing it from one phone viewport is
   how the first pass shipped a window between 520 and 700 where the bubble ran
   off the right.

   Prefixed with `.dd-tooltip` purely for specificity: these have to outrank the
   placement modifiers above, including the two-class combinations. */
@media (max-width: 35rem) {
  .dd-tooltip.dd-tooltip--wrap::after,
  .dd-tooltip.dd-tooltip--wrap:hover::after,
  .dd-tooltip.dd-tooltip--wrap:focus-visible::after,
  .dd-tooltip.dd-tooltip--wrap:has(:focus-visible)::after {
    position: fixed;
    inset: auto 1rem 1rem 1rem;
    width: auto;
    max-width: none;
    transform: none;
  }

  .dd-tooltip.dd-tooltip--wrap::before {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .dd-tooltip::after,
  :where(.dd-btn--icon[aria-label])::after { transition: opacity var(--dur-fast) var(--ease-soft); transform: translateX(-50%); }
  .dd-tooltip--right::after { transform: translateX(0); }
}

/* ============================================================
   The shared bubble (#265). With the script running, every tooltip on the
   page is shown through this one element — `div.dd-tip`, in the browser's
   top layer — placed wherever it fits (`js/components.js`, "tooltips"). Up
   there nothing around the control can clip it: not an `overflow`, not the
   `transform` a kit button lifts with, not the navbar's `backdrop-filter`,
   each of which trapped the `::after` bubble above.

   Everything above this block is the fallback for a page the script never
   ran on. It stands down under `html.dd-tip-js`, which the script sets as it
   starts — not `dd-js`, which only pages with a scroll reveal get.
   ============================================================ */

/* Not `::before` on `--no-arrow`: there it belongs to the component (the
   tabs' underline), which is the whole point of the modifier. */
html.dd-tip-js .dd-tooltip::after,
html.dd-tip-js :where(.dd-btn--icon[aria-label])::after,
html.dd-tip-js .dd-tooltip:not(.dd-tooltip--no-arrow)::before,
html.dd-tip-js :where(.dd-btn--icon[aria-label]):not(.dd-tooltip--no-arrow)::before {
  display: none;
}

.dd-tip {
  /* A popover's own box is centred on the screen, bordered and scrolls; none
     of that is this. The script sets `left` / `top` and the width ceiling. */
  position: fixed;
  inset: auto;
  margin: 0;
  border: 0;
  overflow: visible;
  box-sizing: border-box;
  width: max-content;
  padding: var(--space-2) var(--space-3);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  line-height: 1.3;
  text-align: left;
  overflow-wrap: break-word;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  pointer-events: none;
  /* Only counts where there is no top layer and the bubble is a fixed box on
     <body> instead. */
  z-index: calc(var(--z-toast) + 1);
  animation: dd-tip-in var(--dur-fast) var(--ease-soft);
}

/* The kit's border triangle, on whichever side the bubble opened, slid along
   it to the control's centre (`--dd-tip-arrow`, set by the script). */
.dd-tip__arrow {
  position: absolute;
  border: 6px solid transparent;
}

.dd-tip[data-side="top"] .dd-tip__arrow {
  top: 100%;
  left: var(--dd-tip-arrow);
  translate: -50% 0;
  border-top-color: var(--ink);
}

.dd-tip[data-side="bottom"] .dd-tip__arrow {
  bottom: 100%;
  left: var(--dd-tip-arrow);
  translate: -50% 0;
  border-bottom-color: var(--ink);
}

.dd-tip[data-side="right"] .dd-tip__arrow {
  right: 100%;
  top: var(--dd-tip-arrow);
  translate: 0 -50%;
  border-right-color: var(--ink);
}

.dd-tip[data-side="left"] .dd-tip__arrow {
  left: 100%;
  top: var(--dd-tip-arrow);
  translate: 0 -50%;
  border-left-color: var(--ink);
}

/* The last resort under 35rem is anchored to the viewport, not the control,
   so there is nothing for an arrow to point at. */
.dd-tip[data-side="strip"] .dd-tip__arrow {
  display: none;
}

/* It fades in over the last 4px of its way out from the control, as the
   `::after` bubble does. Keyframes rather than a transition: a popover goes
   from `display: none` to shown, which replays an animation every time and
   never starts a transition. */
.dd-tip[data-side="top"] { --dd-tip-from: 0 4px; }
.dd-tip[data-side="bottom"] { --dd-tip-from: 0 -4px; }
.dd-tip[data-side="right"] { --dd-tip-from: -4px 0; }
.dd-tip[data-side="left"] { --dd-tip-from: 4px 0; }

@keyframes dd-tip-in {
  from {
    opacity: 0;
    translate: var(--dd-tip-from, 0 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .dd-tip { animation: none; }
}
