/*
 * A directory, dressed the way its first tenant is.
 *
 * This shares a design system with the Vibe Code Detector rather than merely
 * resembling one: the tokens below are that site's, name for name and value
 * for value, so the two read as one studio and a third can be added without
 * anybody choosing a new grey. The rules it inherits are worth restating,
 * because every one of them is a position:
 *
 *   - Dark ground, hard contrast. Not a dark *mode* bolted onto a light
 *     design; the light palette is the variant.
 *   - Monospace for everything the machine says: the interface, the counts,
 *     the labels, the headings. A proportional face only where there are
 *     paragraphs to read.
 *   - Softly rounded corners and hairline rules. No shadows at all: depth
 *     comes from surface value and a 1px line.
 *   - No gradient anywhere, on any element, ever.
 *   - One accent, and it means "this one" — never decoration.
 *
 * The one thing this site has to do that the detector does not is show a
 * collection. Everything below the shared tokens is about that: a grid of
 * cards that stays legible at one entry and at forty, and a filter bar that
 * is a real control rather than a search box that hides half the list.
 */

:root {
  color-scheme: dark;

  --paper:    #0b0b0c;   /* the ground */
  --paper-2:  #121214;   /* raised: panels, cards */
  --paper-3:  #1a1a1d;   /* inset: fields, code, wells */

  --ink:      #f4f4f5;
  --ink-2:    #a4a3ab;
  /* Contrast-checked at 4.5:1 against --paper-3, the lightest surface it is
     used on. This is the floor for anything that is still text rather than
     texture; do not darken it. */
  --ink-3:    #82818a;

  --rule:     #26262b;
  --rule-2:   #3a3941;

  --red:      #ff4d2e;
  --red-2:    #ff7256;
  --red-soft: #2a1109;
  --green:    #3fb950;
  --amber:    #d99a2b;

  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --measure: 38rem;
  --gutter: clamp(1rem, 3.5vw, 2.5rem);
  /* Softened, but nowhere near the component-kit default: 10px reads
     considered, 24px reads scaffolded, and 0 reads like a spreadsheet. */
  --radius: 10px;
  --radius-sm: 7px;
}

@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;

    /*
     * Not pure white. The dark palette has three distinct surfaces, and a
     * light one with two of them set to #fff leaves cards sitting on the page
     * with nothing but a hairline to say they are cards — which on a page
     * that is mostly cards is the whole layout gone.
     */
    --paper:    #f8f7f4;
    --paper-2:  #ffffff;
    --paper-3:  #efeeea;

    --ink:      #0b0b0c;
    --ink-2:    #545149;
    --ink-3:    #6e6c66;

    --rule:     #e2e0da;
    --rule-2:   #c6c3ba;

    /* Solved against --paper-3, the darkest of the three light surfaces. */
    --red:      #c73515;
    --red-2:    #a82d12;
    --red-soft: #fdeae4;
    --green:    #1f7a34;
    --amber:    #8a5f10;
  }
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--mono);
  font-size: .9375rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  font-variant-ligatures: none;
}

a {
  color: inherit;
  text-decoration-color: var(--rule-2);
  text-underline-offset: .22em;
  text-decoration-thickness: 1px;
  transition: color .1s linear, text-decoration-color .1s linear;
}
a:hover { color: var(--red); text-decoration-color: var(--red); }

h1, h2, h3 {
  margin: 0;
  font-family: var(--mono);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -.045em;
}

::selection { background: var(--red); color: var(--paper); }

:where(a, button, input, textarea, summary):focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- structure */

.shell {
  max-width: 78rem;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* A page of prose is a column, not a canvas. */
.shell-narrow { max-width: 52rem; }

.eyebrow {
  font-size: .6875rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 1rem;
}

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

/* Anything that lands mid-page needs room above it for the sticky rail. */
[id] { scroll-margin-top: 5rem; }

/* ------------------------------------------------------------------- header */

/*
 * A rail across the top: opaque, full-bleed, hairline under it. Deliberately
 * not the detached pill with a blur behind it — the tool this directory was
 * built around flags those on other people's sites, and it would be a poor
 * showroom that wore one.
 */
.masthead-wrap {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}

.masthead {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: .75rem 0;
  flex-wrap: wrap;
}

.brand {
  display: flex;
  align-items: center;
  gap: .6rem;
  text-decoration: none;
  color: inherit;
}
.brand:hover { color: inherit; }
.brand svg { display: block; width: 24px; height: 24px; flex: none; }

.brand-name {
  font-size: .75rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  font-weight: 600;
  line-height: 1.35;
}
.brand-tag {
  display: block;
  font-size: .6875rem;
  letter-spacing: .04em;
  text-transform: none;
  font-weight: 400;
  color: var(--ink-3);
}

.masthead nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}
.masthead nav a {
  text-decoration: none;
  color: var(--ink-3);
  font-size: .75rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: .5rem .75rem;
  border-radius: var(--radius-sm);
  transition: color .1s linear, background .1s linear;
}
.masthead nav a:hover { color: var(--ink); background: var(--paper-3); }
.masthead nav a[aria-current="page"] { color: var(--red); }

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

.hero { padding: clamp(2.5rem, 7vw, 4.5rem) 0 clamp(1.5rem, 4vw, 2.25rem); }

.hero h1 {
  font-size: clamp(2rem, 6vw, 4rem);
  max-width: 17ch;
  font-weight: 600;
  letter-spacing: -.055em;
  margin-bottom: 1.25rem;
  text-wrap: balance;
}

.hero p {
  max-width: 44rem;
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.6;
  margin: 0 0 1.25rem;
}
.hero p strong { color: var(--ink); font-weight: 600; }

/*
 * The promise, in a box, because it is the only reason to use a directory
 * rather than a search engine. A hairline box and a 2px mark in the accent —
 * two pixels on that edge and not four, because the four-pixel accent strip
 * is itself one of the tells the tool downstairs flags.
 */
.pledge {
  font-family: var(--sans);
  font-size: .9375rem;
  color: var(--ink-2);
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--red);
  padding: .9rem 1.1rem;
  max-width: 44rem;
  margin: 0;
}
.pledge strong { color: var(--ink); font-weight: 600; }
.pledge a { color: var(--ink); }

/* ------------------------------------------------------------------ filters */

/*
 * The filter bar.
 *
 * Rendered hidden and revealed by the script, because a control that filters
 * a list is a lie when there is no script to filter it with. Everything it
 * filters is in the HTML already, so the page is complete and indexable
 * before this appears — and if the script never loads, all that is lost is
 * the ability to narrow a list you can already see in full.
 */
.filters {
  /*
   * Deliberately a little less than the rail is tall, and at a lower z-index,
   * so the bar tucks *under* the masthead rather than meeting it. A value
   * that is too large leaves a few pixels of scrolling page showing between
   * two opaque bars, which is the more obvious failure and the one you cannot
   * fix without measuring the rail — which CSS cannot do.
   */
  position: -webkit-sticky;
  position: sticky;
  top: 3.25rem;
  z-index: 10;
  background: var(--paper);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: .85rem 0;
  margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}
.filters[hidden] { display: none; }

.filters-row {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
}

/* Field and its label share one bordered box, so the pair reads as one
   control: one raised surface, one hairline, no shadow. */
.search {
  display: flex;
  align-items: stretch;
  flex: 1 1 16rem;
  min-width: 0;
  border: 1px solid var(--rule-2);
  border-radius: var(--radius);
  background: var(--paper-2);
  transition: border-color .1s linear;
}
.search:focus-within { border-color: var(--red); }
.search-mark {
  display: grid;
  place-items: center;
  padding-left: .85rem;
  color: var(--ink-3);
  font-size: .875rem;
}
.search input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--mono);
  font-size: .9375rem;
  padding: .7rem .85rem;
}
.search input:focus { outline: none; }
.search input::placeholder { color: var(--ink-3); }
/* The browser's own clear button is a different design system's idea of a
   cross. There is a real button next to it that does the same thing. */
.search input::-webkit-search-cancel-button { display: none; }

.search-clear {
  appearance: none;
  border: 0;
  border-left: 1px solid var(--rule-2);
  background: transparent;
  color: var(--ink-3);
  font: inherit;
  font-size: .8125rem;
  padding: 0 .9rem;
  cursor: pointer;
  border-radius: 0 var(--radius) var(--radius) 0;
  transition: color .1s linear, background .1s linear;
}
.search-clear:hover { color: var(--ink); background: var(--paper-3); }
.search-clear[hidden] { display: none; }

/*
 * A row of hard-edged cells rather than a floating pill. The selected one is
 * filled in the accent, so which slice of the list you are looking at is the
 * single most saturated thing on the page — which is what it should be.
 */
.segmented {
  display: flex;
  border: 1px solid var(--rule-2);
  border-radius: var(--radius);
  width: max-content;
  max-width: 100%;
  overflow: hidden;
  scrollbar-width: none;
}
.segmented::-webkit-scrollbar { display: none; }

.segmented button {
  appearance: none;
  border: 0;
  border-right: 1px solid var(--rule-2);
  background: transparent;
  padding: .6rem .95rem;
  font: inherit;
  font-size: .6875rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
  cursor: pointer;
  white-space: nowrap;
  transition: color .1s linear, background .1s linear;
}
.segmented button:last-child { border-right: 0; }
.segmented button:hover { color: var(--ink); background: var(--paper-3); }
.segmented button[aria-pressed="true"] {
  background: var(--red);
  color: var(--paper);
  font-weight: 600;
}

/* A checkbox that reads as a control rather than as a form field. */
.toggle {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: .75rem;
  letter-spacing: .04em;
  color: var(--ink-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: .55rem .8rem;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color .1s linear, color .1s linear;
}
.toggle:hover { border-color: var(--rule-2); color: var(--ink); }
.toggle input { margin: 0; accent-color: var(--red); }

.count {
  font-size: .75rem;
  letter-spacing: .04em;
  color: var(--ink-3);
  margin-left: auto;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.count strong { color: var(--ink); font-weight: 600; }

/* -------------------------------------------------------------------- cards */

/*
 * auto-fill, not auto-fit.
 *
 * With auto-fit a single card stretches to the full 78rem of the shell and
 * looks like a mistake, which is exactly the state this directory launches in
 * and will return to every time somebody filters hard. auto-fill keeps the
 * column width honest, so one result is one card-sized card with space beside
 * it rather than a banner.
 */
.grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
  gap: 1rem;
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .6rem;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.25rem 1.35rem 1.35rem;
  transition: border-color .1s linear;
}
.card:hover { border-color: var(--rule-2); }
.card[hidden] { display: none; }

/*
 * The whole card is the target, but only the title is the link.
 *
 * A stretched pseudo-element over the card keeps the click area large without
 * wrapping a paragraph, a list and three labels inside one <a> — which is
 * what makes a card unusable with a screen reader and impossible to select
 * text in. Anything else in the card that has to stay clickable is lifted
 * back above it.
 */
.card-title {
  font-size: 1.125rem;
  letter-spacing: -.03em;
  margin: 0;
}
.card-title a { text-decoration: none; }
.card-title a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
}
.card:hover .card-title a { color: var(--red); }

.card-tagline {
  margin: 0;
  font-family: var(--sans);
  font-size: .9375rem;
  color: var(--ink-2);
  line-height: 1.5;
}

.card-meta {
  display: flex;
  align-items: center;
  gap: .5rem .75rem;
  flex-wrap: wrap;
  margin-top: auto;
  padding-top: .85rem;
  border-top: 1px solid var(--rule);
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/*
 * The status badge.
 *
 * "Live" is a hairline and the ink colour, not a green pill: it is the normal
 * case, and the normal case should not shout. "In the works" gets the amber,
 * because it is the one thing on a card that will disappoint somebody who
 * clicks without reading, and it has to be the thing they read first.
 */
.badge {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  border: 1px solid var(--rule-2);
  border-radius: 999px;
  padding: .15rem .55rem;
  font-size: .625rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.badge::before {
  content: "";
  width: .4rem;
  height: .4rem;
  border-radius: 50%;
  background: currentColor;
}
.badge-live { color: var(--green); border-color: var(--green); }
.badge-building { color: var(--amber); border-color: var(--amber); }

/*
 * An entry with nothing to open reads as a note rather than as a product.
 *
 * The dashed border alone was not enough — at a glance six cards looked like
 * six apps, which on a list where five of them cannot be opened is the whole
 * point missed. So the unbuilt ones lose their raised surface as well and sit
 * flat on the page ground. That is the same depth cue the rest of the site
 * uses, run backwards: a card you can use is lifted, a card you cannot is not.
 */
.card-building {
  background: transparent;
  border-style: dashed;
  border-color: var(--rule-2);
}
.card-building:hover { border-color: var(--ink-3); }
.card-building .card-title a { color: var(--ink-2); }
.card-building .card-tagline { color: var(--ink-3); }

.card-cat { color: var(--ink-3); }

/* Nothing matched. Says what to do about it, which is the only thing an
   empty state is for. */
.empty {
  border: 1px dashed var(--rule-2);
  border-radius: var(--radius);
  padding: clamp(2rem, 6vw, 3.5rem) 1.5rem;
  text-align: center;
  color: var(--ink-2);
  font-family: var(--sans);
}
.empty[hidden] { display: none; }
.empty p { margin: 0 0 .75rem; }
.empty p:last-child { margin: 0; }

/* --------------------------------------------------------------- app pages */

.app-head {
  padding: clamp(2rem, 6vw, 3.5rem) 0 clamp(1.25rem, 3vw, 2rem);
  border-bottom: 1px solid var(--rule);
}
.app-head h1 {
  font-size: clamp(1.875rem, 5.5vw, 3.25rem);
  letter-spacing: -.055em;
  margin-bottom: .85rem;
  text-wrap: balance;
}
.app-head .app-tagline {
  font-family: var(--sans);
  font-size: 1.125rem;
  color: var(--ink-2);
  margin: 0 0 1.5rem;
  max-width: 44rem;
}

.app-actions {
  display: flex;
  align-items: center;
  gap: .75rem 1rem;
  flex-wrap: wrap;
}

.btn {
  appearance: none;
  background: var(--red);
  color: var(--paper);
  border: 1px solid var(--red);
  padding: .7rem 1.5rem;
  font: inherit;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  cursor: pointer;
  border-radius: var(--radius-sm);
  text-decoration: none;
  display: inline-block;
  transition: background .1s linear, border-color .1s linear;
}
.btn:hover { background: var(--red-2); border-color: var(--red-2); color: var(--paper); }

.btn-quiet {
  background: transparent;
  color: var(--ink);
  border-color: var(--rule-2);
}
.btn-quiet:hover { background: var(--paper-3); border-color: var(--ink-3); color: var(--ink); }

.app-body {
  display: grid;
  gap: clamp(2rem, 5vw, 3rem);
  padding: clamp(2rem, 5vw, 3rem) 0;
  align-items: start;
}
@media (min-width: 58rem) {
  .app-body { grid-template-columns: minmax(0, 1fr) 20rem; }
}

.prose p {
  font-family: var(--sans);
  font-size: 1rem;
  color: var(--ink-2);
  line-height: 1.65;
  max-width: 62ch;
  margin: 0 0 1.1rem;
}
.prose p strong { color: var(--ink); font-weight: 600; }
.prose h2 {
  font-size: 1.375rem;
  margin: 2.25rem 0 .85rem;
}
.prose h2:first-child { margin-top: 0; }
.prose ul {
  margin: 0 0 1.1rem;
  padding-left: 1.1rem;
  max-width: 62ch;
  font-family: var(--sans);
  color: var(--ink-2);
}
.prose li { margin-bottom: .5rem; }
.prose li::marker { color: var(--ink-3); }
.prose a { color: var(--ink); }

.prose code, .hero code, .facts code {
  font-family: var(--mono);
  font-size: .9em;
  background: var(--paper-3);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: .05em .35em;
  color: var(--ink);
  overflow-wrap: anywhere;
}

/*
 * The disclosure panel.
 *
 * Every listing answers the same four questions in the same order and in the
 * same place, so that comparing two apps is reading down a column rather than
 * hunting through two pages of marketing. The order is deliberate: cost
 * first, because it is what was promised; then account, then where the work
 * happens, then what is kept.
 */
.facts {
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.25rem 1.35rem;
  position: sticky;
  top: 5rem;
}
.facts dl { margin: 0; }
.facts dt {
  font-size: .6875rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: .3rem;
}
.facts dd {
  margin: 0 0 1.1rem;
  font-family: var(--sans);
  font-size: .875rem;
  color: var(--ink-2);
  line-height: 1.55;
}
.facts dd:last-child { margin-bottom: 0; }
.facts dd strong { color: var(--ink); font-weight: 600; display: block; }

.tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin: 0;
  padding: 0;
}
.tags li {
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: .2rem .6rem;
  font-size: .6875rem;
  color: var(--ink-3);
  letter-spacing: .04em;
}

/* ------------------------------------------------------------------- bands */

/* A full-width run of prose with a rule above it, used on /about. */
.band {
  border-top: 1px solid var(--rule);
  padding: clamp(2rem, 5vw, 3rem) 0;
}

.rules {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: rule;
}
.rules li {
  counter-increment: rule;
  border-top: 1px solid var(--rule);
  padding: 1.1rem 0 1.1rem 3rem;
  position: relative;
  max-width: 62ch;
  font-family: var(--sans);
  color: var(--ink-2);
}
.rules li:last-child { border-bottom: 1px solid var(--rule); }
.rules li::before {
  content: counter(rule, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 1.15rem;
  font-family: var(--mono);
  font-size: .75rem;
  color: var(--red);
  letter-spacing: .04em;
}
.rules strong {
  display: block;
  color: var(--ink);
  font-family: var(--mono);
  font-weight: 600;
  letter-spacing: -.02em;
  margin-bottom: .3rem;
}

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

.colophon {
  border-top: 1px solid var(--rule);
  margin-top: clamp(3rem, 8vw, 5rem);
  padding: 2.5rem 0 3.5rem;
  font-size: .8125rem;
  color: var(--ink-3);
}
.colophon-grid {
  display: flex;
  gap: 3rem;
  flex-wrap: wrap;
  justify-content: space-between;
}
.colophon a { color: var(--ink-2); }
.colophon p {
  margin: 0 0 .6rem;
  max-width: 42ch;
  font-family: var(--sans);
  line-height: 1.6;
}
.colophon .links {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  font-size: .75rem;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.colophon .links a { text-decoration: none; }
.colophon .links a:hover { color: var(--red); }

.colophon .studio {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  font-size: .6875rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* -------------------------------------------------------------------- small */

@media (max-width: 46rem) {
  /* Stacked, and no longer sticky: two sticky bars on a phone leave about
     four hundred pixels for the list they are filtering. */
  .filters { position: static; }
  .filters-row { align-items: stretch; }
  .search { flex-basis: 100%; }
  .segmented {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    width: 100%;
    overflow: visible;
  }
  .segmented button:nth-child(3n) { border-right: 0; }
  .segmented button:nth-child(-n+3) { border-bottom: 1px solid var(--rule-2); }
  .count { margin-left: 0; }
  .facts { position: static; }
  .grid { grid-template-columns: 1fr; }
}
