/* ------------------------------
   Project cards — grid, list view, badges
   ------------------------------
   Shared with the public Explore listing (templates/explore.html), which
   cannot load app.css: that sheet also lays out the app shell (`main
   { display: flex }`, `body { overflow: hidden }`), which would rearrange
   a plain document page — the same reason token-preview.css is its own
   file. Extend this file, not app.css, for anything a project card needs;
   The kebab lives here too, now that Explore's cards have one — added
   client-side by explore-card-menu.mjs, since what it offers depends on the
   account. Its panel contents (.project-menu__*) came along with it: the
   project page's own header menu uses the same rules, and app.html loads
   this sheet, so one copy still serves both. */

/* Measure, centring and gutters come from .container on the markup — here
   and on .projects-grid below — so the Projects page and Explore resolve to
   the same column without either sheet restating the width. */
.projects-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--pf-space-4);
  margin-bottom: var(--pf-space-6);
}

/* Title names the list (Your Projects, or a team workspace). When the
   account belongs to a team, a sort-shaped select sits beside it to
   switch. Collection controls — view, sort, search — sit opposite.
   New project lives in the top bar, because it is the page's primary
   action, not a filter on the list. */
.projects-heading {
  display: flex;
  align-items: center;
  gap: var(--pf-space-4);
  min-width: 0;
  flex-wrap: wrap;
}

.projects-heading h1 {
  margin: 0;
  font-size: var(--pf-text-2xl);
  font-weight: var(--pf-weight-semibold);
  letter-spacing: -0.02em;
}

/* Person identity above a card grid. The in-app member profile
   (/app/members/{id}) is the only page that uses it now, and it lives
   here rather than in app.css so the card grid and the heading share
   one sheet. */
.author-head {
  display: flex;
  align-items: center;
  gap: var(--pf-space-4);
  margin-bottom: var(--pf-space-6);
  min-width: 0;
}

.projects-header > .author-head {
  margin-bottom: 0;
  flex: 1 1 12rem;
}

/* A member profile: identity is the subject of the page, so it takes the
   full first row (thumb, name, meta, follow). View / sort / search are
   filters on the list, so they sit on the row below, closer to the cards
   they change than to the person they do not. */
.projects-header--member {
  flex-direction: column;
  align-items: stretch;
  gap: var(--pf-space-6);
}

.projects-header--member > .author-head {
  flex: none;
}

.author-head__avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: 56px;
  height: 56px;
  overflow: hidden;
  border-radius: var(--pf-radius-full);
  background: var(--pf-color-primary);
  color: var(--pf-color-on-primary);
  font-family: var(--pf-font-display);
  font-size: var(--pf-text-xl);
  font-weight: var(--pf-weight-semibold);
  line-height: 1;
}

/* After the [data-avatar-tint] rules in tokens.css, so a picture wins over
   the tint whatever index the author was given. */
.author-head__avatar--image {
  background: none;
}

.author-head__avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.author-head__avatar.skel-bone {
  border-radius: var(--pf-radius-full);
}

.author-head__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--pf-space-1);
  min-width: 0;
}

.author-head__text h1 {
  margin: 0;
  font-size: var(--pf-text-2xl);
  font-weight: var(--pf-weight-semibold);
  letter-spacing: -0.02em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.author-head__text p {
  margin: 0;
  font-size: var(--pf-text-xs);
  color: var(--pf-color-muted);
}

/* Sits with the identity, not with search and sort: following is about
   this person. margin-left: auto parks it at the end of the head so the
   name stays the subject and the control recedes until you reach for it. */
.author-head__follow {
  flex: none;
  flex-shrink: 0;
  margin-left: auto;
}

.projects-actions {
  display: flex;
  align-items: center;
  gap: var(--pf-space-3);
  /* Wraps onto a second line rather than pushing the page sideways. The
     header above already wraps, which drops this whole block under the
     heading — but with four controls in it that is not always enough, and a
     row that cannot break is a row that overflows. */
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* Sort control. Shaped like .search-field beside it — same border, radius
   and height — because they are the same kind of thing: something you set
   about the list, next to something you set about the list. */
.sort-control {
  display: flex;
  align-items: center;
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
}

.sort-control:focus-within {
  border-color: var(--pf-color-primary);
}

.sort-select {
  padding-block: var(--pf-space-1);
  padding-inline: var(--pf-space-1) var(--pf-space-3);
  border: none;
  border-radius: inherit;
  /* NOT `background: none`. A transparent <select> leaves Chrome to draw the
     dropdown itself, and it draws a white popup with white text in dark mode —
     the same bug the share dialog's role picker had. An explicit background
     is what makes the options legible. */
  background: var(--pf-color-bg);
  color: var(--pf-color-ink);
  font: inherit;
  font-size: var(--pf-text-sm);
  cursor: pointer;
}

.sort-select:focus-visible {
  outline: none;
}

/* Segmented control. The 1px gap over a border-colored background is the
   same hairline trick the card grid uses, so the divider between the two
   buttons matches the rules everywhere else. */
.view-toggle {
  display: flex;
  gap: 1px;
  background: var(--pf-color-border-strong);
  border: 1px solid var(--pf-color-border-strong);
  /* The group is the rounded object, not its halves: overflow clips the two
     square buttons to it, so the divider between them stays a straight
     hairline instead of pinching at four extra corners. Safe to clip here —
     unlike the list container below, nothing inside this ever flies out. */
  border-radius: var(--pf-radius-sm);
  overflow: hidden;
}

.view-toggle__btn {
  display: grid;
  place-items: center;
  padding: var(--pf-space-1) var(--pf-space-3);
  border: none;
  border-radius: var(--pf-radius-none);
  background: var(--pf-color-bg);
  color: var(--pf-color-muted);
  cursor: pointer;
  transition: background var(--pf-duration-fast) var(--pf-ease-out),
              color var(--pf-duration-fast) var(--pf-ease-out);
}

.view-toggle__btn:hover {
  color: var(--pf-color-ink);
}

.view-toggle__btn[aria-pressed="true"] {
  background: var(--pf-color-ink);
  color: var(--pf-color-bg);
}

/* Explore's stand-in for the Projects page's "+ New project" button — same
   slot in .projects-actions, same height as the view-toggle beside it. */
.search-field {
  display: flex;
  align-items: center;
  gap: var(--pf-space-2);
  /* Grows into spare room, gives it back under pressure, and may shrink below
     its content's natural width — the last part is what min-width: 0 buys, and
     without it a flex item refuses to go below its content and overflows. */
  flex: 1 1 14rem;
  max-width: 22rem;
  min-width: 0;
  padding: var(--pf-space-1) var(--pf-space-3);
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
  color: var(--pf-color-muted);
}

.search-field:focus-within {
  border-color: var(--pf-color-primary);
  color: var(--pf-color-ink);
}

.search-field input {
  /* Was a fixed 14rem, which is what made the toolbar overflow: an input that
     cannot shrink turns every pixel the viewport loses into horizontal scroll.
     It now fills whatever the field is given and no more. */
  width: 100%;
  min-width: 0;
  border: none;
  background: none;
  color: inherit;
  font-family: var(--pf-font-body);
  font-size: var(--pf-text-sm);
}

.search-field input:focus {
  outline: none;
}

/* Safari/Chrome's built-in clear "x" on type="search" fields, restyled to
   match the rest of the app's flat, borderless icon buttons instead of the
   default grey circle. */
.search-field input::-webkit-search-cancel-button {
  cursor: pointer;
}

/* Cards, not a table. The old hairline treatment (1px gap over a border
   colored background) made a single project read as one full-width row. */
.projects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--pf-space-4);
}

.project-card {
  position: relative; /* anchors .project-card__link and the grid-view kebab */
  display: flex;
  flex-direction: column;
  gap: var(--pf-space-3);
  padding: var(--pf-space-3);
  background: var(--pf-color-surface);
  border: 1px solid var(--pf-color-border);
  border-radius: var(--pf-radius-sm);
  text-align: left;
  font-family: var(--pf-font-body);
  color: var(--pf-color-ink);
  transition: border-color var(--pf-duration-fast) var(--pf-ease-out),
              background var(--pf-duration-fast) var(--pf-ease-out);
}

/* The card itself is no longer the interactive element — .project-card__link
   is — so its hover state has to react to that descendant instead of to
   itself. */
.project-card:has(.project-card__link:hover) {
  border-color: var(--pf-color-border-strong);
  background: var(--pf-color-surface-sunken);
}

/* A transparent overlay covering the whole card: this is the actual click
   and keyboard target. It replaced a <button> wrapping everything, because
   the kebab menu below is itself interactive, and interactive content
   can't nest inside a <button> OR an <a href> — browsers handle clicks on
   it inconsistently either way. This is a sibling of the kebab, not an
   ancestor, so there's no nesting at all. */
.project-card__link {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: pointer;
}

.project-card__link:focus-visible {
  outline: 2px solid var(--pf-color-primary);
  outline-offset: -2px; /* inset — an outer ring would be clipped by list rows above/below */
}

/* Grid view: the card switches from a flex column to a small named-area
   grid, placing the kebab beside the title instead of floating over the
   thumbnail. A thumbnail is the project's OWN color tokens, so a muted-grey
   icon sitting on top of it has no guaranteed contrast — some palettes would
   make it unreadable or invisible. Anchoring to the title row instead of
   using pixel offsets means this never drifts if the name wraps, the font
   size changes, or the "shared" badge appears. */
.projects-grid[data-view="grid"] .project-card {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "thumb thumb"
    "title menu";
  /* Tighter than the card's own gap, which still separates the thumbnail
     from the text block below it (see the thumb's margin). Three lines of
     text want to read as one group, not as three stacked rows. */
  row-gap: var(--pf-space-2);
}

/* Explore, Projects, and a waiting skeleton all carry byline + stats.
   Author-page cards do not — every card there is by the person named
   above — so the extra row is gated on :has() rather than given to every
   card, or those would grow an empty band. */
.projects-grid[data-view="grid"] .project-card:has(.project-card__stats) {
  grid-template-areas:
    "thumb thumb"
    "title menu"
    "by    stats";
}

/* The one step that stays wide: thumbnail to text is a change of subject,
   the three text lines are not. */
.projects-grid[data-view="grid"] .project-card__thumb {
  margin-bottom: var(--pf-space-1);
}

.projects-grid[data-view="grid"] .project-card__thumb { grid-area: thumb; }

.projects-grid[data-view="grid"] .project-card__body {
  grid-area: title;
  min-width: 0; /* still required for the name's ellipsis under a grid parent, same as under flex */
}

.projects-grid[data-view="grid"] .project-card__by { grid-area: by; }
.projects-grid[data-view="grid"] .project-card__stats {
  grid-area: stats;
  justify-self: end;
}

.projects-grid[data-view="grid"] .project-card__menu {
  grid-area: menu;
  justify-self: end;
  align-self: center;
}

/* ---- thumbnail ---- */

/* 16:9, which is what the settings dialog asks a cover image to be — so an
   uploaded cover fills this exactly rather than being cropped again on
   arrival. The generated color bands take the same box; a card is one shape
   whether or not the project has a picture. */
.project-card__thumb {
  display: flex;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--pf-color-border);
  overflow: hidden;
}

/* cover, not contain: the source was already cropped to 16:9 in the browser
   before it was stored, so there is nothing to letterbox — this only absorbs
   the rounding between that and whatever width the grid gives the card. */
.project-card__thumb--image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project-card__band {
  flex: 1;
  min-width: 0;
}

/* No color tokens — and no tokens at all — land here. The hue is seeded
   from the project id so a given project keeps its texture between visits. */
.project-card__thumb--empty {
  background-color: light-dark(hsl(var(--thumb-hue) 24% 92%), hsl(var(--thumb-hue) 18% 20%));
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0 5px,
    light-dark(hsl(var(--thumb-hue) 28% 84%), hsl(var(--thumb-hue) 22% 27%)) 5px 10px
  );
}

/* ---- card text ---- */

.project-card__body {
  display: flex;
  align-items: center;
  gap: var(--pf-space-2);
  min-width: 0;
}

/* The card's three lines are a hierarchy, and each step down changes both
   size and weight so none of them competes with the one above:
     name   14px semibold, ink
     byline 12px regular,  muted   (see .byline in site.css)
     date   11px mono,     muted
   line-height is pinned on all three so the row-gap between them is the only
   thing setting their spacing — inheriting the body's 1.6 would pad each box
   by a different amount at each size and the steps would read unevenly. */
.project-card__name {
  font-size: var(--pf-text-sm);
  font-weight: var(--pf-weight-semibold);
  line-height: 1.3;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-card__updated,
.project-card__created {
  font-size: var(--pf-text-2xs);
  line-height: 1.3;
  color: var(--pf-color-muted);
  white-space: nowrap;
}

/* Eye and a number. The crossed-out eye is not decoration: it says the count
   is NOT APPLICABLE rather than zero — an unpublished project has no page
   anyone could have been on. */
.project-card__views {
  display: flex;
  align-items: center;
  gap: var(--pf-space-1);
  font-size: var(--pf-text-2xs);
  line-height: 1.3;
  color: var(--pf-color-muted);
  white-space: nowrap;
}

/* display: flex, not the default inline: min-width has no effect on an inline
   box, so as a <span> this could not clamp the byline inside it and a long
   author name ran straight through the Created column beside it. */
.project-card__by {
  display: flex;
  min-width: 0;
  line-height: 1.3;
}

/* Byline and inline like live here as well as in site.css. That sheet is
   what the public Explore documents share (listing, author, project), so
   the copies there stay for the project page, which does not load this.
   The in-app Explore view is the other way around: it loads this and not
   site.css, and a card is a card — see the header of this file. */

/* On the listing this sits inside a card whose whole surface is covered by
   .project-card__link, so it has to come out from under that overlay to be
   clickable at all — the same position/z-index pairing the kebab needs. */
.byline {
  position: relative;
  /* 2, not 1: .project-card__link is also z-index 1 and comes LATER in the
     DOM, so at an equal index the overlay wins the tie and swallows the
     click. */
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--pf-space-2);
  max-width: 100%;
  min-width: 0;
  color: var(--pf-color-muted);
  font-size: var(--pf-text-xs);
}

.byline:hover {
  color: var(--pf-color-ink);
}

/* Deliberately the same 26px circle as .user-menu__avatar in app.css and as
   .byline__avatar in site.css: the same object — an account's picture —
   should not change shape or size with the page it lands on. Three copies
   because no one sheet reaches all three places; this one is loaded by both
   /app and the public Explore listing, which is why it cannot describe
   itself as the app's. */
.byline__avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: 26px;
  height: 26px;
  overflow: hidden;
  border-radius: var(--pf-radius-full);
  background: var(--pf-color-primary);
  color: var(--pf-color-on-primary);
  font-family: var(--pf-font-display);
  font-size: var(--pf-text-xs);
  font-weight: var(--pf-weight-semibold);
  line-height: 1;
}

.byline__avatar--image {
  background: none;
}

.byline__avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.byline__name {
  font-family: var(--pf-font-body);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Metadata that happens to be pressable. No button chrome; z-index so the
   overlay link does not swallow the click. */
.like--inline {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--pf-space-1);
  padding: 0;
  border: none;
  background: none;
  color: var(--pf-color-muted);
  font: inherit;
  font-size: var(--pf-text-2xs);
  line-height: 1.3;
  cursor: pointer;
}

.like--inline:hover,
.like--inline:focus-visible {
  color: var(--pf-color-danger);
}

.like--inline.is-liked {
  color: var(--pf-color-danger);
}

.like--inline:disabled {
  cursor: default;
  opacity: 1; /* the count still has to be read; disabled is "you may not press", not "this is gone" */
}

.like--inline:disabled:hover,
.like--inline:disabled:focus-visible {
  color: var(--pf-color-muted);
}

/* Heart and eye, as a pair of counts. Flex on a card (one cell, end-aligned);
   contents in list view so each count takes its own column. */
.project-card__stats {
  display: flex;
  align-items: center;
  gap: var(--pf-space-3);
}

.projects-grid[data-view="list"] .project-card__stats {
  display: contents;
}

/* No columns to label when the cards are cards, and no dates either: a date
   is worth a column when you are scanning rows and worth nothing on a card you
   are looking at one of. The sort control reaches the same ordering from here. */
.projects-grid[data-view="grid"] .projects-head,
.projects-grid[data-view="grid"] .project-card__updated,
.projects-grid[data-view="grid"] .project-card__created {
  display: none;
}

/* ---- list view ---- */

/* A single stretch column rather than `display: block`, so each row (a
   <div>, itself a grid) stretches to the container's full width — which is
   what keeps its columns aligned with the heading row below. */
.projects-grid[data-view="list"] {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}

/* One template, shared by the heading row and every card, so the columns line
   up without either side knowing the other's widths. Thumbnail, name, kebab —
   the two date columns are gone, and what a row is sorted by lives in the
   control above the list rather than in a column repeated down it. */
.projects-head,
.projects-grid[data-view="list"] .project-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 8rem 4rem 4rem 8rem 8rem 2rem;
  align-items: center;
  gap: var(--pf-space-4);
}

/* Dropped in list view. At 24px tall a cover is too small to tell one project
   from another, so it was spending a column to say nothing. */
.projects-grid[data-view="list"] .project-card__thumb {
  display: none;
}

/* The kebab is pinned to the last track rather than left to auto-placement.
   Auto-placement only keeps it on the row while the number of visible items
   happens to equal the number of columns — add an element to the card, or hide
   one at a breakpoint and forget to adjust the tracks, and it silently wraps to
   a row of its own. Naming its cell means the row is stated rather than
   arithmetic that has to keep coming out right. */
.projects-grid[data-view="list"] .project-card__menu {
  grid-column: -2;
  grid-row: 1;
}

.projects-head {
  padding: var(--pf-space-2) var(--pf-space-4);
  border-bottom: 1px solid var(--pf-color-border);
  background: var(--pf-color-surface-sunken);
  font-size: var(--pf-text-xs);
  color: var(--pf-color-muted);
  letter-spacing: var(--pf-tracking-loose);
}

.projects-grid[data-view="list"] .project-card {
  padding: var(--pf-space-2) var(--pf-space-4);
  border: none;
  border-bottom: 1px solid var(--pf-color-border);
  background: var(--pf-color-surface);
  /* A row is not a card here — the container is the box. */
  border-radius: var(--pf-radius-none);
}

/* The container is rounded but deliberately not clipped: the app's list rows
   carry a kebab whose menu is absolutely positioned and would be cut off by
   an overflow: hidden. So the two rows at the ends carry the outer corners
   themselves, which is also what keeps their opaque backgrounds from
   squaring off the container's border. */
.projects-grid[data-view="list"] .projects-head {
  border-radius: var(--pf-radius-sm) var(--pf-radius-sm) 0 0;
}

.projects-grid[data-view="list"] .project-card:last-child {
  border-bottom: none;
  border-radius: 0 0 var(--pf-radius-sm) var(--pf-radius-sm);
}

/* Columns are dropped by the width of the CONTAINER, not of the window — the
   in-app Projects page draws this table in a pane that is the viewport minus
   the sidebar, so the two numbers differ by ~220px and only one of them is
   the one that decides whether the columns fit. See the container declared on
   .projects-page (app.css) and on .explore (site.css).

   Each threshold is the width below which the NAME column would be squeezed
   under ~200px, which is where a project name stops being readable. The full
   template's fixed tracks come to 34rem, plus six gaps and the row's own
   padding: 672px before the name gets anything at all. It was switching at
   720px of viewport, which inside the app pane meant the table spent the
   whole 721-940px band overflowing its container with a zero-width name. */
@container (max-width: 940px) {
  /* Dates first. The sort control above the list still reaches both
     orderings, so what is lost here is a column and not an ability. */
  .projects-head,
  .projects-grid[data-view="list"] .project-card {
    grid-template-columns: minmax(0, 1fr) 8rem 4rem 4rem 2rem;
  }

  .projects-grid[data-view="list"] .project-card__updated,
  .projects-grid[data-view="list"] .project-card__created {
    display: none;
  }

  /* Name, By, Likes, Views, Last Updated, Created, kebab — positions in THIS
     table's heading row, which is why the selector has to say which table.
     .users-list, .members-list and .users-projects reuse this same markup
     with their own columns, and a bare .projects-head reached all of them:
     the accounts table lost its "Images" heading to this rule while the
     column of numbers under it stayed, because only the heading matched. */
  .projects-grid:not(.users-list):not(.members-list):not(.users-projects)
    .projects-head span:nth-child(5),
  .projects-grid:not(.users-list):not(.members-list):not(.users-projects)
    .projects-head span:nth-child(6) {
    display: none;
  }
}

@container (max-width: 650px) {
  /* The byline goes too: the author is on the card in grid view, and at this
     width a name and its two counts are as much as a row can carry. */
  .projects-head,
  .projects-grid[data-view="list"] .project-card {
    grid-template-columns: minmax(0, 1fr) 4rem 4rem 2rem;
  }

  .projects-grid[data-view="list"] .project-card__by,
  .projects-grid:not(.users-list):not(.members-list):not(.users-projects)
    .projects-head span:nth-child(2) {
    display: none;
  }
}

/* A phone. Two 4rem counts and a kebab leave a name 240px of room in a 304px
   container, and below that the row simply cannot fit — which is where the
   narrow template was itself overflowing. The counts are the least of what a
   row says, so they are what goes. */
@container (max-width: 420px) {
  .projects-head,
  .projects-grid[data-view="list"] .project-card {
    grid-template-columns: minmax(0, 1fr) 2rem;
  }

  .projects-grid[data-view="list"] .project-card__stats .like--inline,
  .projects-grid[data-view="list"] .project-card__views,
  .projects-grid:not(.users-list):not(.members-list):not(.users-projects)
    .projects-head span:nth-child(3),
  .projects-grid:not(.users-list):not(.members-list):not(.users-projects)
    .projects-head span:nth-child(4) {
    display: none;
  }
}

.badge {
  display: inline-block;
  margin-left: var(--pf-space-2);
  padding: 0 var(--pf-space-2);
  background: var(--pf-color-primary);
  color: var(--pf-color-on-primary);
  font-size: var(--pf-text-xs);
}

/* The demo is a reference, not a project of the user's — outlined rather
   than filled, so it reads as a label instead of competing with the primary
   actions on the same row. */
.badge--demo {
  background: none;
  border: 1px solid var(--pf-color-border-strong);
  color: var(--pf-color-muted);
}

/* Published to /explore. Distinct from demo and shared, since a project can
   be both shared with a teammate and public. */
.badge--public {
  background: var(--pf-color-surface-sunken);
  border: 1px solid var(--pf-color-border-strong);
  color: var(--pf-color-muted);
}

/* ---- kebab ---- */

.project-card__menu {
  /* position:relative rather than static: it keeps this a normal in-flow
     element (both views place it as a real grid item — list view's 5th
     column, grid view's "menu" area), while still establishing a stacking
     context so z-index reliably wins over .project-card__link's z-index:1
     sitting beneath it. Without this, the overlay would swallow clicks
     meant for the kebab.
     3, not 2: .byline and .like--inline are also 2 so they beat that overlay.
     At a tie the later grid row (.project-card__by, under the kebab) paints
     over the open panel, which hangs into that row. */
  position: relative;
  z-index: 3;
}

/* Every kebab is 3, so a later card's trigger would paint over this panel —
   the panel's own z-index cannot escape this stacking context. Open is the
   one case the panel has to win, so only then does this step above the rest. */
.project-card__menu:has(.project-card__menu-trigger[aria-expanded="true"]) {
  z-index: 4;
}

.project-card__menu-trigger {
  display: flex;
  align-items: center;
  padding: var(--pf-space-1);
  border: none;
  background: none;
  color: var(--pf-color-muted);
  cursor: pointer;
}

.project-card__menu-trigger:hover,
.project-card__menu-trigger[aria-expanded="true"] {
  color: var(--pf-color-ink);
}

/* Reuses .project-menu__item / __id / __divider (defined above, for the
   project page's own kebab) for the panel's contents — only the trigger and
   the panel's position are specific to the card context. */
.project-card__menu-panel {
  position: absolute;
  top: calc(100% + var(--pf-space-2));
  right: 0;
  z-index: 60;
  min-width: 13rem;
  padding: var(--pf-space-2) 0;
  background: var(--pf-color-surface);
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
  box-shadow: var(--pf-shadow-lg);
}

.project-menu__item {
  display: flex;
  align-items: center;
  gap: var(--pf-space-3);
  width: 100%;
  padding: var(--pf-space-2) var(--pf-space-4);
  border: none;
  border-radius: var(--pf-radius-sm);
  background: none;
  color: var(--pf-color-ink);
  font-family: var(--pf-font-body);
  font-size: var(--pf-text-sm);
  text-align: left;
  cursor: pointer;
}

.project-menu__submenu-host {
  position: relative;
}

/* Flies out to the RIGHT, which is the conventional side and the one there is
   room on: the parent panel is left-aligned under a trigger near the start of
   the toolbar. The account menu's submenu goes left only because that panel is
   flush with the viewport edge. */
.project-menu__panel--sub {
  top: calc(-1 * var(--pf-space-2));
  /* Overlapped by the border width so the two panels share one seam rather
     than showing a double rule or floating apart. */
  left: calc(100% - 1px);
  min-width: 10rem;
}

.project-menu__item--parent[aria-expanded="true"] {
  background: var(--pf-color-surface-sunken);
}

/* Chevron drawn from borders rather than set as a glyph — Outfit has no
   reliable arrow, and this lets it point at the side the submenu opens on. */
.project-menu__item--parent::after {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  margin-left: auto;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.55;
}

/* Icons read as decoration, not label text — hidden from the accessible
   name so a screen reader hears "Export JSON", not "download Export JSON". */
.project-menu__item ui-icon {
  flex: none;
  color: var(--pf-color-muted);
}

.project-menu__item--danger ui-icon {
  color: currentColor;
}

.project-menu__item:hover {
  background: var(--pf-color-surface-sunken);
}

.project-menu__item--danger {
  color: var(--pf-color-danger);
}

.project-menu__item--danger:hover {
  background: var(--pf-color-danger-soft);
}

.project-menu__id {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pf-space-3);
  padding: var(--pf-space-2) var(--pf-space-4);
}

.project-menu__id-label {
  display: flex;
  align-items: center;
  gap: var(--pf-space-1);
  white-space: nowrap;
  font-size: var(--pf-text-xs);
  color: var(--pf-color-muted);
  letter-spacing: var(--pf-tracking-loose);
}

/* A button, not static text — its whole purpose is to be copied when
   telling an AI agent over MCP which project to use. */
.project-menu__id-value {
  display: flex;
  align-items: center;
  gap: var(--pf-space-1);
  padding: 1px var(--pf-space-2);
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
  background: var(--pf-color-surface-sunken);
  color: var(--pf-color-ink);
  font-family: var(--pf-font-mono);
  font-size: var(--pf-text-xs);
  cursor: pointer;
}

.project-menu__id-label ui-icon,
.project-menu__id-value ui-icon {
  flex: none;
  color: var(--pf-color-muted);
}

.project-menu__id-value:hover {
  border-color: var(--pf-color-primary);
  color: var(--pf-color-primary-text);
}

.project-menu__divider {
  margin: var(--pf-space-2) 0;
  border-top: 1px solid var(--pf-color-border);
}


/* ------------------------------
   Pager
   ------------------------------
   Shared by Projects and Explore (this sheet is loaded by both, which is
   why it lives here). Self-contained rather than composed from .btn — the
   two pages' .btn bases differ (site.css vs app.css), and a control this
   small is cheaper to own than to reconcile. */

.pager {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--pf-space-2);
  margin-top: var(--pf-space-6);
}

.pager:empty {
  display: none; /* one page: no furniture */
}

.pager__button {
  min-width: 2.1rem;
  padding: var(--pf-space-1) var(--pf-space-2);
  border: 1px solid var(--pf-color-border-strong);
  border-radius: var(--pf-radius-sm);
  background: none;
  font-family: var(--pf-font-body);
  font-size: var(--pf-text-sm);
  color: var(--pf-color-muted);
  cursor: pointer;
  transition: color var(--pf-duration-fast) var(--pf-ease-out),
              border-color var(--pf-duration-fast) var(--pf-ease-out),
              background var(--pf-duration-fast) var(--pf-ease-out);
}

.pager__button:hover:not(:disabled):not([aria-current]) {
  color: var(--pf-color-ink);
  border-color: var(--pf-color-ink);
}

/* The current page is a fact, not an action — inverted like the app's
   primary actions, and inert (no handler is attached to it). */
.pager__button[aria-current="page"] {
  background: var(--pf-color-ink);
  border-color: var(--pf-color-ink);
  color: var(--pf-color-bg);
  cursor: default;
}

.pager__button:disabled {
  opacity: 0.4;
  cursor: default;
}

.pager__gap {
  color: var(--pf-color-faint);
  font-size: var(--pf-text-sm);
}

/* ---- waiting cards (public Explore first paint) ---- */

/* A bone the public listing can ship in HTML. The in-app lists use
   <ui-skeleton>, whose pulse lives in a shadow root and cannot paint
   until that module upgrades; this page's silhouette has to be on screen
   with the first stylesheet. Same pulse, same token, two hosts. */
.skel-bone {
  display: block;
  position: relative;
  overflow: hidden;
  border-radius: var(--pf-radius-sm);
  background: var(--pf-color-surface-sunken);
  pointer-events: none;
  animation: skel-pulse 2s ease-in-out infinite;
}

@keyframes skel-pulse {
  50% { opacity: 0.5; }
}

@media (prefers-reduced-motion: reduce) {
  .skel-bone {
    animation: none;
  }
}

.project-card__thumb.skel-bone {
  border-radius: 0;
}

.skel-bone--title { width: 70%; height: 0.9em; }
.skel-bone--by { width: 8rem; height: 0.8em; }
.skel-bone--stat { width: 2.5rem; height: 0.8em; }
.skel-bone--stat:first-child { width: 2rem; }
.skel-bone--date { width: 6rem; height: 0.8em; }
.skel-bone--menu { width: 1.25rem; height: 1.25rem; }

/* JS visitors: silhouettes until explore-list.mjs pages the real cards.
   Without JS the silhouettes hide and every card shows — the listing is
   still a complete document. data-js is set by a blocking script on the
   public Explore page, before this grid paints. */
html:not([data-js]) .project-card--skel {
  display: none !important;
}

html[data-js] #explore-grid:not([data-ready]) > .project-card:not(.project-card--skel) {
  display: none !important;
}
