/* The landing page.

   One rule the whole sheet follows: the grid adapts to how many products there
   actually are, rather than assuming a full row. Two products used to sit in a
   three column track with the third column empty, which read as a mistake. */

:root {
  --bg: #070b16;
  --bg-elevated: #0e1526;
  --bg-tile: rgba(255, 255, 255, 0.028);
  --bg-tile-hover: rgba(255, 255, 255, 0.055);
  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.16);
  --text: #eef2fb;
  --text-muted: #94a1bd;
  --brand: #5b8cff;
  --tile-accent: #2f6df6;
  --radius: 16px;

  --dot: rgba(255, 255, 255, 0.05);
  --tile-sheen: rgba(255, 255, 255, 0.05);
  --tile-inset: rgba(255, 255, 255, 0.06);
  --tile-rest: 0 1px 2px rgba(0, 0, 0, 0.28);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.34), 0 12px 32px -16px rgba(0, 0, 0, 0.7);
  --shadow-lift: 0 2px 4px rgba(0, 0, 0, 0.3), 0 20px 44px -18px rgba(0, 0, 0, 0.8);

  --max-width: 1120px;
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  color-scheme: dark;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f7f9fd;
    --bg-elevated: #ffffff;
    --bg-tile: #ffffff;
    --bg-tile-hover: #ffffff;
    --border: rgba(16, 24, 40, 0.09);
    --border-strong: rgba(16, 24, 40, 0.18);
    --text: #101a33;
    --text-muted: #5b6884;
    --brand: #2f6df6;
    --dot: rgba(16, 24, 40, 0.055);
    --tile-sheen: rgba(16, 24, 40, 0.02);
    --tile-inset: rgba(255, 255, 255, 0.9);
    --tile-rest: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 16px -8px rgba(16, 24, 40, 0.12);
    --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 10px 28px -16px rgba(16, 24, 40, 0.2);
    --shadow-lift: 0 2px 4px rgba(16, 24, 40, 0.06), 0 18px 40px -20px rgba(16, 24, 40, 0.28);
    color-scheme: light;
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* Dots sit under the two colour washes so the page has depth without
     anything that has to be animated or repainted on scroll. */
  background:
    radial-gradient(1100px 560px at 50% -14%, rgba(91, 140, 255, 0.18), transparent 62%),
    radial-gradient(820px 440px at 88% 2%, rgba(139, 92, 246, 0.13), transparent 58%),
    radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 0),
    var(--bg);
  background-size: auto, auto, 30px 30px, auto;
  color: var(--text);
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::selection {
  background: color-mix(in srgb, var(--brand) 40%, transparent);
  color: var(--text);
}

a {
  color: inherit;
}

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 10px 16px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: 0 0 10px 0;
  text-decoration: none;
}

.skip-link:focus {
  left: 0;
}

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
  border-radius: 8px;
}

/* -------------------------------------------------------------------- nav */

.nav {
  position: sticky;
  top: 0;
  z-index: 20;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  background: color-mix(in srgb, var(--bg) 76%, transparent);
  border-bottom: 1px solid var(--border);
}

.nav__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 14px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  min-width: 0;
  border-radius: 12px;
}

/* The header mark is the same file the tab shows (/assets/favicon.svg) rather
   than a second drawing of the logo, so the two cannot drift apart. The SVG
   carries its own rounded corners; the radius here just clips the edges at the
   same proportion (14 of 64) and gives a failed load a tile to sit on instead
   of a broken-image glyph. The gradient and the letter it replaced are gone. */
.brand__mark {
  display: block;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: 22%;
  background: var(--bg-elevated);
  box-shadow: 0 4px 14px -4px color-mix(in srgb, var(--brand) 70%, transparent);
}

.brand__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.brand__name {
  font-weight: 650;
  font-size: 17px;
  letter-spacing: -0.015em;
  line-height: 1.2;
}

.brand__tagline {
  color: var(--text-muted);
  font-size: 12.5px;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nav__contact {
  flex: none;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 13.5px;
  padding: 7px 13px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-tile);
  white-space: nowrap;
  transition: color 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
}

.nav__contact:hover {
  color: var(--text);
  border-color: var(--border-strong);
  background: var(--bg-tile-hover);
}

/* ----------------------------------------------------------------- support */

/* The strip between the header and main has three states: the ask on a first
   visit, the ad unit once the visitor says yes, and nothing at all once they
   say no. It is a sibling of main rather than its first child because main
   centres its contents, and a top banner pinned by that centring would float in
   the middle of a short page. */
.support {
  max-width: var(--max-width);
  width: 100%;
  margin: 18px auto 0;
  padding: 0 24px;
}

/* The UA hides [hidden] on its own; this is here so a later display rule on the
   strip cannot quietly override the crawler-only unit being invisible. */
.support[hidden] {
  display: none;
}

.support__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding: 14px 18px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--brand) 8%, var(--bg-elevated));
}

.support__ask {
  min-width: 0;
  flex: 1 1 320px;
}

.support__text {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-muted);
  text-wrap: pretty;
}

.support__text strong {
  color: var(--text);
  font-weight: 650;
}

.support__note {
  margin: 4px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-muted);
  opacity: 0.72;
}

.support__actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
}

.support__yes,
.support__no {
  border-radius: 999px;
  padding: 8px 16px;
  font-size: 13.5px;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}

.support__yes {
  background: var(--brand);
  color: #fff;
  font-weight: 600;
  box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--brand) 80%, transparent);
}

.support__yes:hover {
  background: color-mix(in srgb, var(--brand) 86%, #000);
}

.support__no {
  color: var(--text-muted);
  border: 1px solid var(--border);
}

.support__no:hover {
  color: var(--text);
  border-color: var(--border-strong);
  background: var(--bg-tile-hover);
}

/* The ad unit brings its own frame, so the card dressing is removed and only a
   floor is kept. min-height reserves the slot while the fill is in flight, which
   stops the hero jumping down when an ad lands. */
.support--ad .support__inner {
  display: block;
  padding: 0;
  border: 0;
  background: none;
}

.support--ad .adsbygoogle {
  display: block;
  width: 100%;
  min-height: 90px;
}

@media (max-width: 520px) {
  .support__actions {
    width: 100%;
  }

  .support__yes,
  .support__no {
    flex: 1;
    text-align: center;
  }
}

/* ------------------------------------------------------------------- hero */

/* With only a couple of products the grid ends well above the fold, and pinning
   the footer left a large dead gap that read as a broken page. Centring the
   block in the leftover space makes any tile count look intentional. */
main {
  flex: 1;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.hero {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: clamp(52px, 8vw, 88px) 24px 36px;
  text-align: center;
}

.hero__title {
  margin: 0 0 14px;
  font-size: clamp(30px, 5.4vw, 52px);
  line-height: 1.06;
  letter-spacing: -0.035em;
  font-weight: 700;
  text-wrap: balance;
}

.hero__subtitle {
  margin: 0 auto;
  max-width: 52ch;
  color: var(--text-muted);
  font-size: clamp(15px, 1.9vw, 17.5px);
  text-wrap: balance;
  /* The subtitle is stored as multiple lines, so the breaks the author typed
     are shown rather than collapsed. pre-line keeps the newlines and still
     collapses runs of spaces. */
  white-space: pre-line;
}

/* ----------------------------------------------------------------- search */

.search {
  position: relative;
  max-width: 420px;
  margin: 30px auto 0;
}

.search__icon {
  position: absolute;
  left: 15px;
  top: 50%;
  transform: translateY(-50%);
  width: 17px;
  height: 17px;
  fill: none;
  stroke: var(--text-muted);
  stroke-width: 2;
  stroke-linecap: round;
  pointer-events: none;
  transition: stroke 0.18s ease;
}

.search input {
  width: 100%;
  padding: 13px 16px 13px 44px;
  font: inherit;
  font-size: 15px;
  color: var(--text);
  background: color-mix(in srgb, var(--bg-elevated) 80%, transparent);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}

.search input::placeholder {
  color: var(--text-muted);
}

.search input:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--brand) 65%, transparent);
  box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--brand) 22%, transparent);
}

.search:focus-within .search__icon {
  stroke: var(--brand);
}

.search input::-webkit-search-cancel-button {
  filter: grayscale(1) opacity(0.6);
}

.search__status {
  margin: 10px 0 0;
  font-size: 13px;
  color: var(--text-muted);
  min-height: 1.2em;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------- grid */

.products {
  max-width: var(--max-width);
  width: 100%;
  margin: 0 auto;
  padding: 8px 24px 48px;
}

.grid {
  display: grid;
  gap: 14px;
  /* The default is a compact grid that fills whatever width there is. The
     count rules below override it when there are few enough products that a
     filled row would leave holes. */
  grid-template-columns: repeat(auto-fill, minmax(272px, 1fr));
}

/* Few products: centre the row and cap its width so two tiles are not stretched
   to half the viewport each. Scoped to a min-width query so the small-screen
   rules below still collapse everything to one column without a specificity
   fight. */
@media (min-width: 641px) {
  .grid[data-count="1"] {
    grid-template-columns: minmax(0, 1fr);
    max-width: 520px;
    margin-inline: auto;
  }

  .grid[data-count="2"] {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    max-width: 760px;
    margin-inline: auto;
  }

  .grid[data-count="3"] {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    max-width: 1000px;
    margin-inline: auto;
  }
}

/* ------------------------------------------------------------------- tile */

.tile {
  --tile-accent: #2f6df6;
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 19px 20px;
  min-height: 92px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: linear-gradient(180deg, var(--tile-sheen), transparent 62%), var(--bg-tile);
  box-shadow: inset 0 1px 0 var(--tile-inset), var(--tile-rest);
  text-decoration: none;
  overflow: hidden;
  transition:
    transform 0.22s var(--ease),
    border-color 0.22s ease,
    box-shadow 0.22s ease,
    background-color 0.22s ease;
}

/* The accent rail. It fades out towards the bottom instead of stopping, which
   reads as a colour bleeding in rather than a bar stuck on the edge. */
.tile::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  z-index: 1;
  background: linear-gradient(
    180deg,
    var(--tile-accent),
    color-mix(in srgb, var(--tile-accent) 45%, transparent) 55%,
    transparent
  );
}

/* Accent bloom from the icon corner, off until hover. */
.tile::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  z-index: -1;
  background: radial-gradient(360px 150px at 6% 0%, color-mix(in srgb, var(--tile-accent) 20%, transparent), transparent 68%);
  opacity: 0;
  transition: opacity 0.24s ease;
  pointer-events: none;
}

.tile:hover,
.tile:focus-visible {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--tile-accent) 42%, var(--border));
  background-color: var(--bg-tile-hover);
  box-shadow: inset 0 1px 0 var(--tile-inset), var(--shadow-lift);
}

.tile:hover::after,
.tile:focus-visible::after {
  opacity: 1;
}

.tile[hidden] {
  display: none;
}

.tile__icon {
  flex: none;
  position: relative;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tile-accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--tile-accent) 26%, transparent);
  transition: background-color 0.22s ease, border-color 0.22s ease;
}

.tile:hover .tile__icon {
  background: color-mix(in srgb, var(--tile-accent) 20%, transparent);
  border-color: color-mix(in srgb, var(--tile-accent) 36%, transparent);
}

/* The monogram is drawn from an attribute rather than written into the markup,
   so its colour can come from the generated stylesheet. It sits under the
   favicon and is what you see while the server is still fetching it. */
.tile__mono {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 19px;
  line-height: 1;
  letter-spacing: -0.02em;
  color: color-mix(in srgb, var(--tile-accent) 88%, var(--text));
}

.tile__mono::before {
  content: attr(data-mono);
}

.tile__icon-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 5px;
  object-fit: contain;
  border-radius: inherit;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));
}

.tile__body {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
  flex: 1;
  /* Keeps the text column clear of the arrow. The arrow is absolutely
     positioned, so without this a long description runs underneath it on
     hover. Reserved always rather than on hover so nothing reflows mid-gesture. */
  padding-right: 24px;
}

/* Category sits on its own line above the name. It used to be first in the same
   flex row as the name and the badge, which put three things on one 200px line
   and made the badge wrap. */
.tile__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
}

.tile__name {
  min-width: 0;
  font-weight: 600;
  font-size: 15.5px;
  letter-spacing: -0.012em;
  line-height: 1.3;
}

.tile__tag {
  align-self: flex-start;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 650;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--tile-accent) 72%, var(--text));
  background: color-mix(in srgb, var(--tile-accent) 15%, transparent);
}

.tile__badge {
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text);
  background: color-mix(in srgb, var(--tile-accent) 88%, #000);
}

/* Clamped so one long description cannot make its card taller than the rest of
   the row. The ellipsis means the cut is visible rather than silent. */
.tile__description {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--text-muted);
  font-size: 13.5px;
  line-height: 1.45;
  text-wrap: pretty;
}

/* Absolutely positioned on purpose. As a flex child it reserved its width on
   every card even at opacity 0, which cost each tile body 44px and was enough
   to wrap the name row on a two-product layout. */
.tile__arrow {
  position: absolute;
  right: 12px;
  top: 50%;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  color: var(--tile-accent);
  font-size: 16px;
  line-height: 1;
  background: color-mix(in srgb, var(--tile-accent) 12%, transparent);
  opacity: 0;
  transform: translate(-5px, -50%);
  transition: opacity 0.2s ease, transform 0.22s var(--ease);
  pointer-events: none;
}

.tile:hover .tile__arrow,
.tile:focus-visible .tile__arrow {
  opacity: 1;
  transform: translate(0, -50%);
}

/* Few products: roomier cards, so two of them read as deliberate rather than
   as a row that ran out. */
@media (min-width: 641px) {
  .grid[data-count="1"] .tile,
  .grid[data-count="2"] .tile,
  .grid[data-count="3"] .tile {
    padding: 26px 28px;
    gap: 18px;
    min-height: 116px;
  }

  .grid[data-count="1"] .tile__icon,
  .grid[data-count="2"] .tile__icon,
  .grid[data-count="3"] .tile__icon {
    width: 54px;
    height: 54px;
    border-radius: 15px;
  }

  .grid[data-count="1"] .tile__mono,
  .grid[data-count="2"] .tile__mono,
  .grid[data-count="3"] .tile__mono {
    font-size: 23px;
  }

  .grid[data-count="1"] .tile__name,
  .grid[data-count="2"] .tile__name,
  .grid[data-count="3"] .tile__name {
    font-size: 17.5px;
  }

  .grid[data-count="1"] .tile__description,
  .grid[data-count="2"] .tile__description,
  .grid[data-count="3"] .tile__description {
    font-size: 14.5px;
  }
}

.empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: 52px 24px;
  text-align: center;
  color: var(--text-muted);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-tile);
}

/* ----------------------------------------------------------------- footer */

.footer {
  border-top: 1px solid var(--border);
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--bg-elevated) 45%, transparent));
  padding: 26px 24px 30px;
  text-align: center;
  color: var(--text-muted);
  font-size: 13.5px;
}

.footer p {
  margin: 3px 0;
}

/* Same as the hero subtitle: stored lines are shown as typed. */
.footer__text {
  white-space: pre-line;
}

.footer a {
  color: var(--text-muted);
  text-decoration: none;
  border-bottom: 1px solid var(--border-strong);
  transition: color 0.18s ease, border-color 0.18s ease;
}

.footer a:hover {
  color: var(--text);
  border-color: var(--brand);
}

/* ------------------------------------------------------------------ entry */

/* A short staggered settle. Scoped to no-preference and given `backwards` fill
   so nothing is painted before its turn. The reduced-motion block at the end of
   this file zeroes the delay, or the tiles would stay invisible through it. */
@media (prefers-reduced-motion: no-preference) {
  .tile,
  .empty {
    animation: tile-in 0.5s var(--ease) backwards;
  }

  .tile:nth-child(1), .empty:nth-child(1) { animation-delay: 0.04s; }
  .tile:nth-child(2), .empty:nth-child(2) { animation-delay: 0.08s; }
  .tile:nth-child(3), .empty:nth-child(3) { animation-delay: 0.12s; }
  .tile:nth-child(4), .empty:nth-child(4) { animation-delay: 0.16s; }
  .tile:nth-child(5), .empty:nth-child(5) { animation-delay: 0.2s; }
  .tile:nth-child(6), .empty:nth-child(6) { animation-delay: 0.24s; }
  .tile:nth-child(7), .empty:nth-child(7) { animation-delay: 0.28s; }
  .tile:nth-child(8), .empty:nth-child(8) { animation-delay: 0.32s; }
  .tile:nth-child(n + 9) { animation-delay: 0.34s; }
}

@keyframes tile-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@media (max-width: 640px) {
  .nav__inner {
    padding: 12px 16px;
  }

  .hero {
    padding: 46px 16px 28px;
  }

  .products {
    padding: 8px 16px 60px;
  }

  .grid {
    grid-template-columns: 1fr;
    gap: 11px;
  }

  .tile {
    padding: 16px 17px;
  }

  .nav__contact {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    /* Without this the staggered entries would sit invisible for the length of
       their delay before appearing, which is the opposite of the intent. */
    animation-delay: 0s !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }

  .tile:hover,
  .tile:focus-visible {
    transform: none;
  }
}
