/*
 * Scroll Progress
 *
 * Measured from Figma `Line` / `Progress` (191:968) / `Progress Heading`
 * (191:1021), re-synced August 2026 after the sizes, colours and spacing were
 * retuned on the canvas.
 *
 * FIGMA SPECIFIES STATES, NOT SCROLL-TRACKING
 *
 * The "32%" and the one heavier tick in Figma's mockup are just that variant's
 * static content — evidence of what the rail looks like at some scroll
 * position, not a scroll-tracking algorithm to reproduce. This component
 * takes `progress` and `activeId` as props for the same reason Select takes
 * `value` rather than reading a DOM scroll position itself: which container
 * scrolls, whether the page is one document or many, and how "the active
 * section" is defined are all facts only the consuming app has.
 *
 * TICKS ARE 2px TALL AND 24px WIDE, WITH ROUND ENDS
 *
 * Figma's `Line` stroke is 2, `border-width/thick`, not `/default`. Easy to
 * misread as a mistake since every hairline elsewhere in the system is 1px;
 * checked against the node rather than assumed. The instances are 24px wide
 * (`spacing/24`) even though the `Line` component's own frame is still 32 —
 * the instance override is what renders, and it is what this file follows.
 * The stroke is `stroke-linecap="round"`, hence `--radius-full`.
 *
 * THE PERCENTAGE IS TWO TYPE SIZES, NOT ONE
 *
 * The number sits on `type/body-sm` and the `%` sign a rung down on
 * `type/caption`. It looks like a rendering artefact in the Figma export and
 * is not — the two spans carry different bound size variables.
 *
 * THE FLYOUT IS A DISCLOSURE, NOT A MENU
 *
 * No `role="menu"` here, for the same reason `Menu` itself doesn't claim one:
 * that role promises roving-tabindex arrow-key navigation and typeahead, and
 * this component doesn't implement either — its rows sit in normal tab order.
 * The trigger/panel pair follows the WAI-ARIA Disclosure pattern instead:
 * `aria-expanded` + `aria-controls`, nothing more elaborate.
 *
 * REVEAL TRANSITION IS DELIBERATELY PLAIN
 *
 * Opacity and an 8px slide on the shared motion ladder — `--ion-duration-base`
 * and `--ion-ease-out`, the same language every hover state in the system
 * already uses. It reads as a reveal rather than travel, so it stays on `base`
 * rather than taking the `slow` rung. The deeper choreography this
 * component could eventually justify — a staggered reveal per row, easing
 * that matches the rail's own scale — is future work, tracked in
 * docs/motion-system.md, not invented here ahead of that decision.
 */
.ion-scroll-progress {
  position: relative;
  display: inline-flex;
}

/*
 * THE FIXED WIDTH IS WHAT STOPS THE RAIL WALKING SIDEWAYS
 *
 * Without it the trigger sizes to its widest child, which is the percentage,
 * and the percentage changes width as the reader scrolls — 9% to 10%, 99% to
 * 100%. The ticks are centred, so every digit gained slid them half its width
 * across the page. It read as the rail drifting, which is the one thing an
 * indicator of position must never appear to do on its own.
 *
 * `spacing/32` is Figma's own root frame (191:967). 0.29.0 declined to adopt
 * it on the grounds that `100%` would wrap inside 32px — true, but the fix for
 * that is `nowrap` on the percentage, not a fluid container. A long value now
 * overflows the box symmetrically instead of resizing it, so the ticks hold
 * still and the number grows evenly around their axis.
 */
.ion-scroll-progress__trigger {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: var(--spacing-32);
  gap: var(--spacing-8);
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.ion-scroll-progress__trigger:focus-visible {
  outline: var(--border-width-thick) solid var(--border-focus);
  outline-offset: 2px;
}

/*
 * `--text-tertiary` in every state. The rail used to lift to
 * `--text-secondary` on hover; the retuned `Status=Hover` variant keeps the
 * percentage tertiary and lets the panel opening be the whole feedback, so
 * that rule is gone rather than kept as a local invention.
 */
.ion-scroll-progress__percent {
  color: var(--text-tertiary);
  font-family: var(--font-family-sans);
  font-size: var(--type-body-sm);
  line-height: var(--type-body-sm-line-height);
  text-align: center;
  white-space: nowrap;

  /*
   * Proportional digits are a second, smaller source of the same drift: `1` is
   * narrower than `8` in most grotesks, so even 11% to 18% — no digit gained —
   * re-centres the string. `tabular-nums` pins every digit to one advance, so
   * the readout only moves when it genuinely gains a digit. A font without the
   * `tnum` feature ignores this and loses nothing the fixed width above was
   * already covering.
   */
  font-variant-numeric: tabular-nums;
}

.ion-scroll-progress__percent-sign {
  font-size: var(--type-caption);
  line-height: var(--type-caption-line-height);
}

.ion-scroll-progress__rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-8);
}

.ion-scroll-progress__tick {
  width: var(--spacing-24);
  height: var(--border-width-thick);
  background-color: var(--border-strong);
  border-radius: var(--radius-full);
  transition: background-color var(--ion-duration-base) var(--ion-ease-out);
}

.ion-scroll-progress__tick--active {
  background-color: var(--border-stronger);
}

/*
 * Hidden via visibility + opacity, not `display: none` — the transition needs
 * a property to animate, and `visibility` still pulls the panel's buttons out
 * of tab order and the accessibility tree while closed, same as a native
 * `<details>` collapses its content.
 *
 * `width`, not `min-width`: Figma fixes the panel at 220 and truncates each
 * row rather than letting a long heading widen the flyout — see the ellipsis
 * on `__heading`.
 *
 * CAPPED AT 60vh, THE SAME RUNG AS THE POPOVER BODY
 *
 * Figma draws seven rows and stops, because a Figma frame has no viewport to
 * overflow. A real document index is as long as the document, and an
 * uncapped centred panel grows off both edges of the screen at once — the
 * rows a reader most wants are the ones that leave first. `60vh` is not a new
 * number: `.ion-popover__body` already caps against the viewport at exactly
 * that, and this is the same problem.
 *
 * `overscroll-behavior: contain` matters more here than in most flyouts. This
 * component reports reading position, so letting a wheel gesture chain into
 * the document once the list bottoms out would scroll the page — and move the
 * very percentage the reader opened the panel to act on. Keyboard users are
 * unaffected either way: the rows are real buttons in tab order, so Tab
 * scrolls each one into view without touching the wheel.
 *
 * CLOSED IS `display: none`, NOT ONLY INVISIBLE
 *
 * `visibility: hidden` removes the panel from sight and from tab order, but a
 * hidden box still counts toward its scroll container's overflow. A rail near
 * the right edge of a page therefore widened the page by however far the
 * closed panel reached past it — 152px in the demo app — and a phone scrolled
 * sideways into empty space. `display: none` takes it out of layout entirely.
 * The reveal survives: `transition-behavior: allow-discrete` holds `display`
 * until the fade-out finishes, and `@starting-style` gives the fade-in a
 * starting point. A browser without either simply shows and hides it without
 * the fade. `visibility` stays in the transition for the same browsers.
 */
.ion-scroll-progress__panel {
  position: absolute;
  top: 50%;
  z-index: 10;
  display: none;
  flex-direction: column;
  gap: var(--spacing-4);
  width: 220px;
  max-height: 60vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--spacing-8);
  background-color: var(--surface-default);
  border: var(--border-width-default) solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--ion-shadow-shadow-md);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-50%) translateX(var(--ion-scroll-progress-slide));
  transition:
    opacity var(--ion-duration-base) var(--ion-ease-out),
    transform var(--ion-duration-base) var(--ion-ease-out),
    visibility var(--ion-duration-base),
    display var(--ion-duration-base) allow-discrete;
}

/*
 * Placement picks the edge the panel is anchored to and the direction it
 * slides in from — always from the rail outward. `right` is the original
 * behaviour; `left` is for a rail against the right edge of the page, where a
 * rightward panel would leave the screen.
 */
/* stylelint-disable property-disallowed-list -- `side` names the physical
   side the panel opens toward, away from a rail placed against a page edge. */
.ion-scroll-progress__panel--right {
  --ion-scroll-progress-slide: -4px;

  left: 0;
}

.ion-scroll-progress__panel--left {
  --ion-scroll-progress-slide: 4px;

  right: 0;
}
/* stylelint-enable property-disallowed-list */

.ion-scroll-progress__panel[data-open='true'] {
  display: flex;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(-50%) translateX(0);
}

@starting-style {
  .ion-scroll-progress__panel[data-open='true'] {
    opacity: 0;
    transform: translateY(-50%) translateX(var(--ion-scroll-progress-slide));
  }
}

/*
 * `display: block`, not `flex`: `text-overflow: ellipsis` does not apply to an
 * anonymous flex item, so a flex row here would clip the label with no ellipsis
 * at all. Padding plus the body-sm line height gives Figma's 32px row exactly.
 *
 * `flex-shrink: 0` is what makes the panel's `max-height` scroll instead of
 * squash, and the reason is not obvious. A column flex item is normally
 * protected from shrinking past its content by the automatic minimum size —
 * but that protection is dropped for any item whose `overflow` is not
 * `visible`, and the ellipsis above sets `overflow: hidden` on every row. So
 * the rows would have compressed toward 0 to fit the cap, and the panel would
 * never have overflowed at all.
 */
.ion-scroll-progress__heading {
  display: block;
  flex-shrink: 0;
  width: 100%;
  padding: var(--spacing-6) var(--spacing-8);
  background-color: transparent;
  border: var(--border-width-default) solid transparent;
  border-radius: var(--radius-md);
  color: var(--text-tertiary);
  font-family: var(--font-family-sans);
  font-size: var(--type-body-sm);
  line-height: var(--type-body-sm-line-height);
  font-weight: var(--font-weight-regular);
  text-align: start;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--ion-duration-base) var(--ion-ease-out),
    border-color var(--ion-duration-base) var(--ion-ease-out),
    color var(--ion-duration-base) var(--ion-ease-out);
}

@media (hover: hover) {
  .ion-scroll-progress__heading:hover {
    background-color: var(--surface-muted);
    border-color: var(--border-strong);
    color: var(--text-default);
  }
}

.ion-scroll-progress__heading[data-hovered='true'] {
  background-color: var(--surface-muted);
  border-color: var(--border-strong);
  color: var(--text-default);
}

.ion-scroll-progress__heading--selected {
  background-color: var(--surface-muted);
  color: var(--text-secondary);
}

.ion-scroll-progress__heading:focus-visible {
  outline: var(--border-width-thick) solid var(--border-focus);
  outline-offset: -1px;
}

/*
 * Each tick is a background, so forced colours erased the whole rail and left
 * the percentage alone. The ticks are drawn in the text colour, and the one
 * for where you are in Highlight.
 */
@media (forced-colors: active) {
  .ion-scroll-progress__tick {
    forced-color-adjust: none;
    background-color: CanvasText;
  }

  .ion-scroll-progress__tick--active {
    background-color: Highlight;
  }
}
