/*
 * Catalogue: a filterable index, not a grid of cards.
 *
 * With 120+ tools a card grid is unreadable — every tile looks the same and
 * the eye has nothing to scan. Rows give a single strong left edge, the name
 * carries the weight, and the category rail does the narrowing.
 */

/* ------------------------------------------------------------ filter rail */

.catalog-filters {
  position: sticky;
  z-index: 20;
  top: calc(var(--header-height) + 0.5rem);
  padding-block: var(--space-4) var(--space-3);
}

/* -------------------------------------------------------------- omni bar */

/* Three fixed painted pieces form each glass shell. The round caps translate
 * and the centre stretches; neither layout nor corner radii change per frame. */
.omni {
  --omni-knob: 3rem;
  /*
   * The gap reads as "these are two controls", so it has to be wide enough to
   * be unmistakably background rather than a sliver of something. At 0.75rem
   * the round button and the strip's leading edge crowded each other and the
   * strip's own 4px inset looked like a third element wedged between them.
   * It doubles as the entire budget for the rail's elasticity.
   */
  --omni-gap: 1rem;
  position: relative;
  height: var(--omni-knob);
  contain: layout style;
  isolation: isolate;
  overflow: clip;
}
.omni__glass {
  position: absolute;
  top: 0;
  height: 100%;
  width: calc(100% - var(--omni-knob) - var(--omni-gap));
  pointer-events: none;
  color: var(--color-line);
  --omni-fill: var(--color-surface);
}
.omni__glass--search { left: 0; transform-origin: left center; }
.omni__glass--tabs { right: 0; transform-origin: right center; --omni-fill: var(--color-surface-sunken); }
.omni__cap, .omni__bridge {
  position: absolute;
  top: 0;
  height: 100%;
  background: var(--omni-fill);
  border: 1px solid currentColor;
}
.omni__cap { width: calc(var(--omni-knob) / 2); }
.omni__cap--start { border-radius: 999px 0 0 999px; border-right: 0; }
.omni__cap--end { border-radius: 0 999px 999px 0; border-left: 0; }
.omni__bridge { width: calc(100% - var(--omni-knob)); border-inline: 0; }
.omni__glass--search .omni__cap--start { left: 0; }
.omni__glass--search .omni__cap--end { right: 0; }
.omni__glass--search .omni__bridge { left: calc(var(--omni-knob) / 2); transform-origin: left center; }
.omni__glass--tabs .omni__cap--end { right: 0; }
.omni__glass--tabs .omni__cap--start { left: 0; }
.omni__glass--tabs .omni__bridge { right: calc(var(--omni-knob) / 2); transform-origin: right center; }
.omni[data-omni-mode="tabs"] .omni__glass--search .omni__cap--end {
  right: calc(100% - var(--omni-knob));
  transform: none;
}
.omni[data-omni-mode="search"] .omni__glass--tabs .omni__cap--start { left: calc(100% - var(--omni-knob)); }
.omni[data-omni-mode="tabs"] .omni__glass--search .omni__bridge,
.omni[data-omni-mode="search"] .omni__glass--tabs .omni__bridge { transform: scaleX(0); }

/*
 * The shells are opaque. There is no backdrop blur anywhere on this rail.
 *
 * Frosted glass was tried in three places and each one broke differently:
 *
 *   `.omni::before`, one layer across the whole rail — cheapest, because the
 *   filter footprint never changed size, but it also covered the gap between
 *   the two shells. A backdrop filter over a gap is still a backdrop filter,
 *   so once the sticky bar scrolled over the list the gap frosted too and the
 *   round button and the strip read as one continuous piece of glass.
 *
 *   `.omni__glass` clipped to the shape — fixed the gap, but the two shells'
 *   boxes overlap by design (each is nearly the full rail wide), so the clip
 *   could not be relaxed mid-flight without double blurring. Dropping the
 *   filter for the duration instead made the material jump: the page showed
 *   through sharply while the rail moved, then frosted the instant it stopped.
 *
 *   The caps and bridges themselves — geometrically correct, and the filter
 *   region stays fixed because a backdrop filter samples the border box before
 *   the transform. But the frost did not actually render: on a real scroll the
 *   list showed through the button sharply enough to read, which is what a
 *   backdrop filter is supposed to prevent.
 *
 * A 3px-wide effect is not worth a fourth attempt at something that has to be
 * judged by eye and cannot be judged by eye here — the pane never scrolls, so
 * the one condition that makes the filter visible is the one condition that
 * cannot be reproduced. Opaque fills are unconditionally correct: they cover
 * the list cleanly, the two controls are separate because nothing bridges
 * them, and the material is identical at rest and in flight because there is
 * no longer any material that depends on what is behind it.
 *
 * The top highlight and the rim stay. They are plain paint, they carry the
 * same sense of a raised surface, and they render the same everywhere.
 */
html:is(.motion-standard, .motion-full) :is(.omni__cap, .omni__bridge) {
  background: linear-gradient(180deg, color-mix(in srgb, var(--color-surface) 38%, transparent), transparent 65%), var(--omni-fill);
}

.omni[data-omni-mode="search"]:focus-within .omni__glass--search { color: var(--color-accent); }

.omni__unit {
  position: absolute;
  top: 0;
  bottom: 0;
  overflow: hidden;
}
.omni__search-content { position: absolute; inset: 0; }
.omni__unit--search { left: 0; right: calc(var(--omni-knob) + var(--omni-gap)); }
.omni__unit--tabs { right: 0; left: calc(var(--omni-knob) + var(--omni-gap)); }
.omni[data-omni-mode="tabs"] .omni__unit--search { transform: translateX(calc(-100% + var(--omni-knob))); }
.omni[data-omni-mode="tabs"] .omni__search-content { transform: translateX(calc(100% - var(--omni-knob))); }
.omni[data-omni-mode="search"] .omni__unit--tabs { transform: translateX(calc(100% - var(--omni-knob))); }
.omni__hit:focus-visible { outline-offset: -4px; }

/* --- the search half --- */

.omni__icon {
  position: absolute;
  top: 50%;
  left: calc(var(--omni-knob) / 2);
  width: 1.15rem;
  height: 1.15rem;
  color: var(--color-text-muted);
  transform: translate(-50%, -50%);
  transition: color var(--duration-fast) ease;
  pointer-events: none;
}

.omni[data-omni-mode="search"] .omni__icon {
  color: var(--color-accent);
  transform: translate(-50%, -50%) scale(1.055) rotate(-4deg);
}

.omni__field {
  width: 100%;
  height: 100%;
  padding: 0 var(--space-5) 0 var(--omni-knob);
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--color-text);
  font-size: var(--text-md);
  opacity: 0;
  transition: none;
}

.omni[data-omni-mode="search"] .omni__field {
  opacity: 1;
}

.omni__field::placeholder {
  color: var(--color-text-faint);
}

.omni__field:focus {
  outline: none;
  box-shadow: none; /* Focus belongs to the moving glass shell. */
}

/*
 * A query that is still narrowing the list while the search is folded away
 * would otherwise be invisible. The dot says the circle is doing something.
 */
.omni__dot {
  position: absolute;
  top: 0.6rem;
  left: calc(var(--omni-knob) / 2 + 0.42rem);
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 999px;
  background: var(--color-accent);
  opacity: 0;
  transform: scale(0.4);
  transition: opacity var(--dur-fast) ease, transform var(--dur-normal) var(--ease-spring);
  pointer-events: none;
}

.omni[data-omni-query="true"][data-omni-mode="tabs"] .omni__dot {
  opacity: 1;
  transform: none;
}

/* --- the categories half --- */

/*
 * The moving indicator is a single element positioned by JS, so the active
 * pill glides between tabs instead of blinking. Its `width` is written from
 * script — the one exception ANIMATIONS.md 3.9 grants, because no transform
 * turns one tab's box into another's.
 */
.segmented {
  position: relative;
  display: flex;
  gap: 0.125rem;
  height: 100%;
  overflow-x: auto;
  padding: 0.25rem;
  border-radius: 999px;
  opacity: 1;
  transition: none;
  scrollbar-width: none;
}

.omni[data-omni-mode="search"] .segmented {
  opacity: 0;
}

.segmented::-webkit-scrollbar {
  display: none;
}

.segmented__indicator {
  position: absolute;
  z-index: 0;
  top: 0.25rem;
  left: 0;
  height: calc(100% - 0.5rem);
  border-radius: 999px;
  background: var(--color-surface);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
  /* OsuTabControl: the strip and the selected tab's bar both move on
   * OutQuint over TRANSITION_LENGTH = 500ms, and neither overshoots. */
  transition: transform 500ms var(--ease-out-quint), width 500ms var(--ease-out-quint);
  pointer-events: none;
}

.segmented__indicator[data-ready="false"] {
  opacity: 0;
}

/*
 * Tabs share the rail rather than stacking at its left edge.
 *
 * With `flex: none` the eight categories came to 714px inside a 1076px strip
 * and left a third of the glass empty on the right, which tips the whole
 * control's weight to one side on a wide screen. Sharing the space is also
 * what a segmented control is supposed to look like — iOS has always divided
 * it evenly. `min-width: max-content` keeps a label from being squeezed into
 * a wrap; once the natural widths no longer fit, the strip scrolls as before.
 */
.segmented button {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  min-width: max-content;
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding-inline: var(--space-4);
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  font-weight: 500;
  white-space: nowrap;
  transition: color var(--duration-fast) ease;
}

.segmented button:hover {
  color: var(--color-text);
}

.segmented button[aria-selected="true"] {
  color: var(--color-text);
}

.segmented__count {
  color: var(--color-text-faint);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  transition: color var(--duration-fast) ease;
}

.segmented button[aria-selected="true"] .segmented__count {
  color: var(--color-accent);
}

/* What is left of the strip once it is folded. */
.omni__folded {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  display: grid;
  place-items: center;
  width: var(--omni-knob);
  color: var(--color-text-muted);
  opacity: 0;
  transform: scale(0.86) rotate(-6deg);
  transition: none;
  pointer-events: none;
}

.omni[data-omni-mode="search"] .omni__folded {
  opacity: 1;
  transform: scale(1) rotate(0deg);
}

.omni__folded svg {
  width: 1.15rem;
  height: 1.15rem;
}

/*
 * The two press targets. Each covers the circle its half shrinks to, and is
 * live only in the mode where that circle is the thing on screen — otherwise
 * the search button would sit on top of the input it just opened.
 */
.omni__hit {
  position: absolute;
  top: 0;
  z-index: 2;
  width: var(--omni-knob);
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  transition: none;
}

.omni__hit--search { left: 0; }

.omni__hit--tabs { right: 0; }

.omni[data-omni-mode="search"] .omni__hit--search,
.omni[data-omni-mode="tabs"] .omni__hit--tabs {
  pointer-events: none;
}

@media (max-width: 40rem) {
  .omni {
    --omni-knob: 2.75rem;
  }
}

/*
 * The count sits below the filter bar, not inside it. In the bar it rode along
 * with the sticky element and ended up floating over the list, where a bare
 * "122 个工具" reads as a stray label rather than as a result count. Here it
 * belongs to the list it counts, and it scrolls away with it.
 *
 * `max-width` is reset because base.css caps every <p> at the reading measure.
 * That cap is right for prose and wrong here: it made a centred one-line count
 * centre itself inside a 500px box at the left of a 1136px column, which reads
 * as neither centred nor aligned to anything.
 */
.catalog-status {
  max-width: none;
  min-height: 1.4rem;
  margin: 0 0 var(--space-2);
  padding-inline: var(--space-5);
  color: var(--color-text-faint);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

@media (max-width: 40rem) {
  .catalog-status {
    padding-inline: var(--space-3);
  }
}

/* ------------------------------------------------------------- index rows */

.index-list {
  border-top: 1px solid var(--color-line);
}

.index-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: var(--space-4) var(--space-5);
  align-items: center;
  padding: var(--space-4) var(--space-4) var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-line);
  color: inherit;
  text-decoration: none;
  transition: background-color var(--duration-fast) ease;
}

.index-row::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  opacity: 0;
  transition: opacity var(--duration-fast) ease;
}

.index-row {
  position: relative;
  isolation: isolate;
}

.index-row:hover::before,
.index-row:focus-visible::before {
  opacity: 1;
}

.index-row > * {
  position: relative;
  z-index: 1;
}

.index-row__body {
  display: grid;
  gap: 0.2rem;
  min-width: 0;
  transition: transform var(--dur-normal) var(--ease-spring);
}

.index-row:hover .index-row__body {
  transform: translateX(0.3rem);
}

.index-row__name {
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.015em;
}

.index-row__desc {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.index-row__tag {
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  background: var(--color-surface-sunken);
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
  transition: background-color var(--duration-fast) ease, color var(--duration-fast) ease;
}

.index-row:hover .index-row__tag {
  background: var(--color-accent-wash);
  color: var(--color-accent);
}

.index-row__arrow {
  width: 1.5rem;
  color: var(--color-text-faint);
  font-size: var(--text-lg);
  text-align: right;
  transition: transform var(--dur-normal) var(--ease-spring), color var(--duration-fast) ease;
}

.index-row:hover .index-row__arrow {
  color: var(--color-accent);
  transform: translateX(0.25rem);
}

.index-row[hidden] {
  display: none;
}

.catalog-empty {
  padding-block: clamp(3rem, 8vw, 5rem);
  color: var(--color-text-muted);
  text-align: center;
}

@media (max-width: 40rem) {
  .index-row {
    grid-template-columns: minmax(0, 1fr) auto;
    padding-inline: var(--space-3);
  }

  .index-row__arrow {
    display: none;
  }

  .index-row__desc {
    white-space: normal;
  }
}

/*
 * Rows that arrive after a filter change come in with weight rather than a
 * plain fade: a short rise with a little overshoot, staggered across the
 * first rows only so a full reset still settles quickly.
 */
@keyframes row-arrive {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  60% { opacity: 1; }
  to { opacity: 1; transform: none; }
}

html.motion-standard .index-row.is-appearing,
html.motion-full .index-row.is-appearing {
  animation: row-arrive var(--dur-normal) var(--ease-spring) both;
  animation-delay: var(--row-delay, 0ms);
}

html.motion-minimal .index-row.is-appearing {
  animation: row-arrive var(--dur-fast) ease both;
}

html.motion-none .index-row.is-appearing {
  animation: none;
}

html.motion-none .segmented__indicator {
  transition: none;
}

html.motion-minimal .omni__icon,
html.motion-minimal .omni__folded {
  transform: translate(-50%, -50%);
}

html.motion-minimal .omni__folded {
  transform: none;
}

/* ---------------------------------------------------------------- bridge */

/*
 * Matching single-step operations from the advanced surface, shown under the
 * tool rows they belong with. They are deliberately a separate block: an
 * operation runs in place and a tool is a page, and mixing them into one list
 * would make the reader guess which kind of thing they are about to open.
 */
.catalog-bridge {
  margin-top: var(--space-7);
}

.catalog-bridge__head {
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
  margin: 0;
  padding-inline: var(--space-5);
  font-size: var(--text-md);
  font-weight: 600;
}

.catalog-bridge__count {
  color: var(--color-text-faint);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  font-weight: 400;
}

.catalog-bridge__note {
  max-width: none;
  margin: 0.25rem 0 var(--space-3);
  padding-inline: var(--space-5);
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.catalog-bridge__all {
  display: inline-block;
  margin-top: var(--space-4);
  padding-inline: var(--space-5);
  font-size: var(--text-sm);
}

.catalog-bridge__more {
  display: flex;
  justify-content: center;
  padding-top: var(--space-4);
}

@media (max-width: 40rem) {
  .catalog-bridge__head,
  .catalog-bridge__note,
  .catalog-bridge__all {
    padding-inline: var(--space-3);
  }
}
