/* Directory — the filter bar and the tool card.
   The category chips and text filter that narrow the list, the tool card
   in both its closed and expanded states, and the skeleton shown while
   data/tools.json is still in flight.

   Part of the stylesheet split described in index.html; the <link> order
   there is the cascade order, so this file expects everything above it to
   have loaded first. */

/* ---------- Directory controls ---------- */
/* The text filter and the category chips are one control group, so they
   travel together when the bar sticks on a phone. */
.filters-bar {
  margin-bottom: 2rem;
}

.dirsearch {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: min(22rem, 100%);
  margin-bottom: 0.9rem;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  transition: border-color var(--fast) var(--ease);
}
.dirsearch:focus-within {
  border-color: var(--ink);
}
.dirsearch__icon {
  font-size: 1rem;
  color: var(--muted);
}
.dirsearch__input {
  flex: 1;
  min-width: 0;
  border: 0;
  padding: 0;
  background: transparent;
  font: inherit;
  font-size: 0.9375rem;
  color: var(--ink);
}
.dirsearch__input::placeholder {
  color: var(--muted);
}
.dirsearch__input:focus {
  outline: none;
}
/* The native decorations sit oddly inside a pill and duplicate the icon. */
.dirsearch__input::-webkit-search-decoration,
.dirsearch__input::-webkit-search-results-button {
  -webkit-appearance: none;
}

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.filter {
  padding: 0.45rem 0.95rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: color var(--fast) var(--ease),
    border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.filter:hover {
  color: var(--ink);
  border-color: var(--ink);
}
.filter[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
/* A category with nothing in it at the chosen price. It stays on screen and
   keeps its count — "(0)" is the answer to "what's in here?" — but it stops
   inviting a click it cannot honour. */
.filter:disabled {
  opacity: 0.4;
  cursor: default;
}
.filter:disabled:hover {
  color: var(--muted);
  border-color: var(--line);
}

/* Shown only by the <noscript> block in index.html, which flips it to block
   and hides the machinery that never fills. Hidden here rather than with the
   hidden attribute so the noscript stylesheet can reveal it without script. */
.noscript-note {
  display: none;
  max-width: 34rem;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--muted);
}

/* ---------- Grouped catalog ---------- */
/* The headings are what make a 14,700px list navigable: something to scroll
   past and recognise, rather than 112 undifferentiated cards. Generous space
   above each one and almost none below, so a heading reads as belonging to
   the grid under it rather than floating between two. */
.tool-group + .tool-group {
  margin-top: 2.75rem;
}

/* Sticky, because the point of the headings is to answer "where am I?" and a
   heading that has scrolled away cannot. It parks under the floating nav
   (which ends at 5rem) and hands over to the next one on the way past. The
   opaque background and the negative inline margin are what stop cards
   showing through and past the sides of a heading that is no longer in the
   flow. */
.tool-group__title {
  position: sticky;
  top: 5rem;
  z-index: 5;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  margin-inline: -0.75rem;
  margin-bottom: 0.9rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--line);
  background: var(--paper);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.tool-group__count {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--muted);
}

/* ---------- Tool cards ---------- */
/* Cards take their own height rather than stretching to the tallest in the
   row. Stretching aligned the footers neatly, but one expanded card then
   grew every card beside it and left them with a band of dead space. The
   clamped description and price keep the natural heights close enough that
   the ragged edge reads as a grid, not as a mistake. */
.tool-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  align-items: start;
  gap: 1rem;
}

/* Tighter radius than the feature cards, so scale reads as hierarchy. */
.tool-card {
  display: flex;
  flex-direction: column;
  padding: 1.35rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  transition: transform var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.tool-card:hover {
  transform: translateY(-3px);
  border-color: transparent;
  box-shadow: var(--shadow-lift);
}

.tool-card__head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.9rem;
}

.tool-card__logo {
  width: 2.25rem;
  height: 2.25rem;
  flex: none;
  border-radius: 9px;
  object-fit: contain;
  background: var(--surface-2);
  padding: 3px;
}
.tool-card__logo--fallback {
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--muted);
  padding: 0;
}

.tool-card__title {
  min-width: 0;
}

.tool-card__name {
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.3;
}

.tool-card__cat {
  font-size: 0.8125rem;
  color: var(--muted);
}

/* Why this card ranked. Only ever on a search result, and set apart from
   the publisher's own copy below it — this is the site talking, and it
   should be legible as such. */
/* The cost filter sits under the category row as its own group, so the two
   read as different questions rather than one long row of chips. */
.filters--price {
  margin-top: 0.5rem;
}
/* Only the free side takes the green, matching the pricing badge on the
   cards it filters to. Painting the paid chip green too would say the
   opposite of what it does. */
.filters--price .filter[data-price="free"][aria-pressed="true"] {
  border-color: var(--green);
}
.filters--price .filter[data-price="paid"][aria-pressed="true"] {
  border-color: var(--coral);
}

.tool-card__why {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin-bottom: 0.6rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--link);
}
.tool-card__why-icon {
  flex: none;
  width: 0.9rem;
  height: 0.9rem;
  /* The spark is a filled shape; the shared .icon rule strokes its icons. */
  fill: currentColor;
  stroke: none;
  transform: translateY(0.1rem);
}

/* The one card a ?tool= link was pointing at, so arriving on a list of a
   hundred does not mean hunting for the reason you followed the link. */
.tool-card--linked {
  border-color: var(--link);
  box-shadow: 0 0 0 3px light-dark(rgba(47, 94, 174, 0.16), rgba(143, 180, 236, 0.22));
}

.tool-card__tagline {
  font-size: 0.9375rem;
  color: var(--ink);
  margin-bottom: 0.55rem;
}

/* The full catalog runs down one page, so the collapsed card shows
   only as much prose as can be skimmed. The full text is a click away on
   the card's own Details toggle. */
.tool-card__description {
  margin-bottom: 1rem;
  font-size: 0.875rem;
  color: var(--muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}
.tool-card--open .tool-card__description {
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}

.tool-card__badges {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: auto;
  margin-bottom: 0.85rem;
}

.tool-card__badge {
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  background: var(--neutral-wash);
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
}
/* Cost is the first thing most people filter on, so the three pricing
   models get three readings rather than one shared accent. */
.tool-card__badge--free {
  background: var(--green-wash);
  color: var(--green);
}
.tool-card__badge--paid {
  background: var(--coral-wash);
  color: var(--coral-ink);
}

.tool-card__meta {
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
  font-size: 0.8125rem;
  color: var(--muted);
}

/* Show pricing once in either state; opening Details adds the checked date. */
/* Billing qualifications stay visible even on a collapsed card. */
.tool-card__meta--short {
  overflow-wrap: anywhere;
}
.tool-card--open .tool-card__meta--short,
.tool-card:not(.tool-card--open) .tool-card__detail {
  display: none;
}

.tool-card__checked {
  margin-top: 0.35rem;
  font-size: 0.75rem;
  color: var(--muted);
}

.tool-card__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.9rem;
}

/* Copy link and Details, kept together on the right. */
.tool-card__secondary {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

/* Icon-only, because the card's own name is already long and a third worded
   control pushes "Open Semantic Scholar" onto two lines. The accessible name
   carries the tool, so it is never just "button" to a screen reader. */
.tool-card__share {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 0.875rem;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.tool-card__share:hover,
.tool-card__share:focus-visible {
  color: var(--ink);
  border-color: var(--ink);
}
/* Confirmation the eye can catch without a word of layout shift: the same
   green the free-tier badge uses, held for two seconds. */
.tool-card__share.is-copied {
  color: var(--green);
  border-color: var(--green);
}

/* The primary move on every card. The arrow leans out on hover and the
   rule under the label draws in from the left, so it reads as something
   that goes somewhere rather than a line of bold text. */
/* The vertical padding is the tap target, not decoration. Without it this
   is a 22px-high box on a phone — under the 24px WCAG 2.5.8 floor, on the
   one control every card exists to offer. The row is already as tall as the
   copy button beside it, so paying for it costs about a pixel. */
.tool-card__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.tool-card__link-label {
  position: relative;
}
.tool-card__link-label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--base) var(--ease);
}
.tool-card__arrow {
  font-size: 0.9rem;
  transition: transform var(--base) var(--ease);
}
.tool-card__link:hover,
.tool-card__link:focus-visible {
  color: var(--coral);
}
.tool-card__link:hover .tool-card__link-label::after,
.tool-card__link:focus-visible .tool-card__link-label::after {
  transform: scaleX(1);
}
.tool-card__link:hover .tool-card__arrow,
.tool-card__link:focus-visible .tool-card__arrow {
  transform: translate(2px, -2px);
}

.tool-card__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex: none;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--fast) var(--ease),
    border-color var(--fast) var(--ease);
}
.tool-card__toggle:hover {
  color: var(--ink);
  border-color: var(--ink);
}
/* Rotates rather than swapping glyphs, so open and closed are one control
   in two states. */
.tool-card__toggle::after {
  content: "";
  width: 0.32rem;
  height: 0.32rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-1px) rotate(45deg);
  transition: transform var(--base) var(--ease);
}
.tool-card__toggle[aria-expanded="true"]::after {
  transform: translateY(1px) rotate(225deg);
}

.tool-list__loading,
.tool-list__empty {
  grid-column: 1 / -1;
  padding: 2.5rem 0;
  color: var(--muted);
}

/* ---------- Skeleton loading state ---------- */
.tool-card--skeleton {
  pointer-events: none;
}
.skeleton-block {
  display: block;
  border-radius: 6px;
  background: var(--line);
  animation: pulse 1.4s ease-in-out infinite;
}
.skeleton-block--logo {
  width: 2.25rem;
  height: 2.25rem;
  flex: none;
  border-radius: 9px;
}
.skeleton-block--title {
  width: 55%;
  height: 0.95rem;
}
.skeleton-block--line {
  width: 100%;
  height: 0.75rem;
  margin-bottom: 0.5rem;
}
.skeleton-block--line.short {
  width: 65%;
}

@keyframes pulse {
  50% {
    opacity: 0.45;
  }
}

.directory-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
  color: var(--muted);
  font-size: 0.875rem;
}
.directory-summary .filter { flex-shrink: 0; }
.directory-empty__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  margin-top: 1rem;
}

.results-more { display: inline-block; margin-top: 1.5rem; font-weight: 600; }
.tool-card__access { font-size: 0.8125rem; margin-bottom: 0.8rem; color: var(--ink); }
.tool-card__utilities { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; margin-top: 1rem; padding-top: 0.8rem; border-top: 1px solid var(--line); }
.tool-card__compare, .compare-pick { font: inherit; font-size: 0.8125rem; padding: 0.4rem 0.6rem; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-radius: 0.5rem; cursor: pointer; }
.tool-card__compare[aria-pressed="true"] { background: var(--ink); color: var(--paper); }

/* At the cap, the other Compare buttons recede rather than switching off.
   Disabling them stated the limit nowhere, could not be focused or pressed,
   and — since a selection survives in session storage — met people on a
   fresh page with a screenful of dead controls. Pressing one still answers,
   so it stays a button: full contrast on its label, muted frame. */
.tool-card__compare--full {
  border-style: dashed;
  color: var(--muted);
}
/* Same reason as the card's own link: 0.75rem of text is a 19px target.
   The compare button sets this row's height, so the padding is free. */
.tool-card__report { font-size: 0.75rem; padding: 0.3rem 0; }
.tool-card__copy-feedback:not(:empty) { margin-top: 0.75rem; font-size: 0.8125rem; color: var(--ink); }
.tool-card__copy-feedback input { box-sizing: border-box; width: 100%; margin-top: 0.5rem; padding: 0.65rem; font: inherit; background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: 0.4rem; }
.compare-bar { position: fixed; z-index: 60; bottom: 1rem; left: 50%; transform: translateX(-50%); width: min(58rem, calc(100% - 2rem)); box-sizing: border-box; display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; padding: 1rem; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); box-shadow: var(--shadow-nav); }
.compare-picks { display: flex; flex-wrap: wrap; gap: 0.4rem; flex: 1; }
.compare-hint { font-size: 0.75rem; color: var(--muted); }

/* Pressing Compare at the cap changes nothing on the card, so the bar —
   which is where the rule is written and where the three picks are — takes
   one beat of attention. The reduced-motion block in motion.css flattens
   the duration, leaving the colour change alone. */
.compare-bar--full {
  animation: compare-full 900ms var(--ease);
}
@keyframes compare-full {
  0%, 60% { border-color: var(--coral); }
  100% { border-color: var(--line); }
}
.compare-bar--full .compare-hint {
  color: var(--ink);
  font-weight: 600;
}
.has-comparison { padding-bottom: 12rem; }
.compare-dialog { width: min(68rem, calc(100% - 2rem)); max-height: 85dvh; box-sizing: border-box; margin: auto; padding: 1.5rem; color: var(--ink); background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-md); }
.compare-dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
.compare-dialog__head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
.compare-dialog h2 { font-size: 1.5rem; }
.compare-dialog p { font-size: 0.875rem; color: var(--muted); }
.compare-scroll { overflow: auto; margin-top: 1rem; }
.compare-scroll table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.compare-scroll th, .compare-scroll td { min-width: 12rem; padding: 1rem; border: 1px solid var(--line); text-align: left; vertical-align: top; }
.compare-scroll th { background: var(--surface); }
.compare-scroll th:first-child { min-width: 9rem; }

/* On a phone the table is two and a half times the width of its window, so
   scrolling to the third tool used to carry the row labels off the left
   edge — leaving a column of values with nothing saying what they measure.
   The label column stays put instead. The header cell needs the higher
   layer because it is the one place a sticky row and a sticky column meet. */
.compare-scroll th[scope="row"],
.compare-scroll thead th:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
}
.compare-scroll thead th:first-child {
  z-index: 2;
}

.compare-bar[hidden], .results-more[hidden] { display: none; }
