/* ============================================================
   Tai VONG — Portfolio archive (/projects/)

   Loaded only on `body.is-projects`, on top of homepage.css. Every
   token, font and button here comes from the shell design system —
   this file adds the archive-specific layout and nothing else.

   Mobile-first: the base rules are the narrow-viewport layout and
   min-width queries widen it.
   ============================================================ */

/* Most blocks here are display:grid/flex/inline-flex, which beats the UA
   `[hidden] { display: none }` rule. Restore it once for the whole page so
   toggling the `hidden` attribute is always enough to remove something. */
body.is-projects [hidden] { display: none !important; }

/* The archive is the one page on the site that is a grid rather than a column,
   so it gets a wider box than the 1200px the marketing pages use — at 1700px
   the old cap left ~300px of dead margin and still only fitted three cards.
   Nav and footer are widened with it so the page keeps one left edge. */
@media (min-width: 1280px) {
  body.is-projects .hb-container,
  body.is-projects .hb-nav-inner,
  body.is-projects .hb-footer-inner,
  body.is-projects .hb-final-cta { max-width: 1460px; }
}

/* Archive-only tokens. The shell palette is cyan + amber; the archive adds two
   more hues used *only* as wayfinding — a per-category accent rule and a
   status dot — never as decoration or as a background fill. Both themes get
   their own triples: the dark-theme hues are unreadable as text on white. */
body.is-projects {
  --hbp-green-rgb:   15, 122, 82;
  --hbp-violet-rgb:  91, 51, 189;
}

html[data-theme="dark"] body.is-projects {
  --hbp-green-rgb:   63, 207, 142;
  --hbp-violet-rgb:  167, 139, 250;
}

body.is-projects {
  --hbp-green:       rgb(var(--hbp-green-rgb));
  --hbp-green-soft:  rgba(var(--hbp-green-rgb), 0.09);
  --hbp-green-dim:   rgba(var(--hbp-green-rgb), 0.38);
  --hbp-violet:      rgb(var(--hbp-violet-rgb));
  --hbp-violet-soft: rgba(var(--hbp-violet-rgb), 0.09);
  --hbp-violet-dim:  rgba(var(--hbp-violet-rgb), 0.38);
  --hbp-amber-dim:   rgba(var(--hb-amber-rgb), 0.40);
}

/* === HERO === */
.hbp-hero {
  padding-bottom: clamp(36px, 5vw, 56px);
}

/* Vertical rhythm comes from the flex gap rather than per-child margins. The
   original reason was that homepage.css's element-selector reset outranked
   these rules; that reset is :where()-wrapped now, but a single gap is still
   the better way to space a stack. */
/* No max-width / margin-left override here on purpose: the hero has to share
   .hb-container's centred 1200px box with the toolbar and the card grid below,
   otherwise the page has two different left edges. Line length is capped on the
   text itself instead. */
.hbp-hero-inner {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 22px;
}

.hbp-hero-title {
  font-size: clamp(30px, 5.4vw, 48px);
  line-height: 1.1;
}

/* Lede and stats are the same fact told two ways, so on a wide screen they sit
   side by side instead of stacking into 400px of scrolling before the archive. */
.hbp-hero-body {
  display: grid;
  gap: 24px;
  width: 100%;
}

.hbp-hero-lede {
  max-width: 64ch;
}

/* The nav drops its CTA at 900px; this one picks it up there and nowhere else,
   so the page is never without a visible ask and never shows two of them. */
.hbp-hero-cta { display: none; }

@media (max-width: 900px) {
  .hbp-hero-cta { display: inline-flex; margin-top: 2px; }
}

@media (max-width: 640px) {
  .hbp-hero-cta { width: 100%; }
}

.hbp-hero-note {
  max-width: 74ch;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--hb-text-muted);
  padding-left: 14px;
  border-left: 2px solid var(--hbp-amber-dim);
}

/* --- headline figures ---
   No box, no dividers: the numbers carry themselves at size, and the line under
   each says whose number it is so none of them reads as borrowed. */
/* Two by two, not a stack of four: as one column the block ran ~270px past the
   bottom of the lede beside it and the hero read as two unrelated halves. */
.hbp-figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px 24px;
}

@media (max-width: 420px) {
  .hbp-figures { grid-template-columns: minmax(0, 1fr); }
}

.hbp-figure dt {
  display: flex;
  align-items: baseline;
  gap: 7px;
  margin-bottom: 5px;
}

/* 38px against a 17px lede made the right column shout over the left. */
.hbp-figure-value {
  font-family: var(--hb-font-mono);
  font-size: clamp(23px, 2.1vw, 27px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--hb-cyan);
}

.hbp-figure-unit {
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  color: var(--hb-text);
}

.hbp-figure dd {
  font-size: 12px;
  line-height: 1.45;
  color: var(--hb-text-muted);
}

.hbp-hero-cta { display: none; }

@media (max-width: 900px) {
  .hbp-hero-cta { display: inline-flex; margin-top: 2px; }
}

@media (max-width: 640px) {
  .hbp-hero-cta { width: 100%; }
}

.hbp-hero-note {
  max-width: 74ch;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--hb-text-muted);
  padding-left: 14px;
  border-left: 2px solid var(--hbp-amber-dim);
}

@media (min-width: 1040px) {
  .hbp-hero-body {
    grid-template-columns: minmax(0, 1fr) 380px;
    gap: 48px;
    align-items: start;
  }
  /* Back to 2x2 once it is a column rather than a full-width band — and a size
     down, because a 2x2 cell is half as wide and "2018–2026" is nine glyphs. */
}

/* === BROWSE SHELL === */
.hbp-browse {
  padding-top: clamp(40px, 5vw, 64px);
  border-top: 1px solid var(--hb-border);
  background: var(--hb-bg-alt);
}

/* === NO-JS CATEGORY INDEX === */
.hbp-index {
  padding: 20px;
  border: 1px solid var(--hb-border);
  border-radius: var(--hb-radius);
  background: var(--hb-surface);
}

.hbp-index-label,
.hbp-filter-label,
.hbp-search-label {
  display: block;
  font-family: var(--hb-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--hb-text-muted);
  margin-bottom: 9px;
}

.hbp-index-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.hbp-index-list a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--hb-border);
  border-radius: 999px;
  background: var(--hb-surface-2);
  font-family: var(--hb-font-mono);
  font-size: 12.5px;
  color: var(--hb-text-dim);
  transition: border-color 0.15s ease, color 0.15s ease;
}

.hbp-index-list a:hover {
  border-color: var(--hb-cyan-dim);
  color: var(--hb-cyan);
}

.hbp-index-count,
.hbp-chip-count {
  font-size: 11px;
  color: var(--hb-text-muted);
}

/* === BROWSE SHELL: sticky control rail + archive column === */
.hbp-browse-inner {
  display: grid;
  gap: 28px;
}

@media (min-width: 880px) {
  /* Only once projects.js has confirmed the controls work — see the class it
     adds. An explicit column track with a hidden sidebar would otherwise leave
     a permanent empty gutter for anyone without JS. */
  .hbp-browse-inner.is-enhanced {
    grid-template-columns: 218px minmax(0, 1fr);
    gap: 32px;
    align-items: start;
  }
}

@media (min-width: 1120px) {
  .hbp-browse-inner.is-enhanced { grid-template-columns: 244px minmax(0, 1fr); gap: 40px; }
}

.hbp-main { min-width: 0; }

/* === SIDEBAR === */
.hbp-sidebar {
  display: grid;
  /* minmax(0, 1fr) rather than the implicit `auto`: the search field's <input>
     carries a default intrinsic width, and an auto track grows to fit it,
     pushing every row past the rail and clipping the counts. */
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  align-content: start;
  padding: 16px;
  border: 1px solid var(--hb-border);
  border-radius: var(--hb-radius);
  background: var(--hb-surface);
}

.hbp-side-block { min-width: 0; }

@media (min-width: 880px) {
  .hbp-sidebar {
    position: sticky;
    /* nav is 73px and sticky; clear it plus a little air */
    top: 89px;
    max-height: calc(100vh - 105px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .hbp-sidebar::-webkit-scrollbar { width: 6px; }
  .hbp-sidebar::-webkit-scrollbar-track { background: transparent; }
  .hbp-sidebar::-webkit-scrollbar-thumb { background: var(--hb-border-strong); border-radius: 3px; }
}

/* --- search --- */
.hbp-search-field {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  min-height: 42px;
  border: 1px solid var(--hb-border-strong);
  border-radius: var(--hb-radius-sm);
  background: var(--hb-bg);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.hbp-search-field:focus-within {
  border-color: var(--hb-cyan);
  box-shadow: 0 0 0 3px rgba(var(--hb-cyan-rgb), 0.16);
}

.hbp-search-prompt {
  font-family: var(--hb-font-mono);
  font-size: 14px;
  color: var(--hb-cyan);
}

.hbp-search-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--hb-text);
  font-family: var(--hb-font-mono);
  font-size: 13.5px;
  padding: 0;
}

.hbp-search-input::placeholder { color: var(--hb-text-muted); }
.hbp-search-input:focus { outline: none; }
.hbp-search-input::-webkit-search-cancel-button { filter: invert(1) opacity(0.45); }

.hbp-search-hint {
  margin-top: 8px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--hb-text-muted);
}

/* --- category rows: long labels, so they stack and read as a contents list.
   Each carries its band's accent, which is the same colour the group head and
   the cards in that band use. --- */
.hbp-side-list {
  display: grid;
  gap: 2px;
}

.hbp-side-item {
  --hbp-accent: var(--hb-cyan);
  --hbp-accent-soft: var(--hb-cyan-soft);
  --hbp-accent-dim: var(--hb-cyan-dim);
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 34px;
  padding: 6px 9px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--hb-text-dim);
  font-family: var(--hb-font-mono);
  font-size: 12px;
  line-height: 1.35;
  text-align: left;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}

.hbp-side-item[data-group="flagship"],
.hbp-side-item[data-group="research"] {
  --hbp-accent: var(--hb-amber);
  --hbp-accent-soft: var(--hb-amber-soft);
  --hbp-accent-dim: var(--hbp-amber-dim);
}

.hbp-side-item[data-group="current"] {
  --hbp-accent: var(--hbp-green);
  --hbp-accent-soft: var(--hbp-green-soft);
  --hbp-accent-dim: var(--hbp-green-dim);
}

.hbp-side-item[data-group="open-source"] {
  --hbp-accent: var(--hbp-violet);
  --hbp-accent-soft: var(--hbp-violet-soft);
  --hbp-accent-dim: var(--hbp-violet-dim);
}

.hbp-side-item::before {
  content: "";
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 2px;
  background: var(--hbp-accent);
  opacity: 0.45;
  transition: opacity 0.15s ease;
}

.hbp-side-item-label {
  flex: 1 1 auto;
  min-width: 0;
}

.hbp-side-item-count {
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--hb-text-muted);
  font-variant-numeric: tabular-nums;
}

.hbp-side-item:hover {
  background: var(--hb-surface-2);
  color: var(--hb-text);
}

.hbp-side-item:hover::before { opacity: 0.8; }

.hbp-side-item[aria-pressed="true"] {
  background: var(--hbp-accent-soft);
  border-color: var(--hbp-accent-dim);
  color: var(--hbp-accent);
}

.hbp-side-item[aria-pressed="true"]::before { opacity: 1; }
.hbp-side-item[aria-pressed="true"] .hbp-side-item-count { color: inherit; }

/* --- technology chips --- */
.hbp-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.hbp-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 27px;
  padding: 0 9px;
  border: 1px solid var(--hb-border);
  border-radius: 999px;
  background: var(--hb-surface-2);
  color: var(--hb-text-dim);
  font-family: var(--hb-font-mono);
  font-size: 11px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}

.hbp-chip:hover {
  border-color: var(--hb-cyan-dim);
  color: var(--hb-text);
}

.hbp-chip[aria-pressed="true"] {
  border-color: var(--hb-cyan);
  background: var(--hb-cyan-soft);
  color: var(--hb-cyan);
}

.hbp-chip-count { font-size: 10.5px; color: var(--hb-text-muted); }
.hbp-chip[aria-pressed="true"] .hbp-chip-count { color: var(--hb-cyan-dim); }

/* --- result bar --- */
.hbp-resultbar {
  display: grid;
  gap: 10px;
  justify-items: start;
  padding-top: 14px;
  border-top: 1px solid var(--hb-border);
}

/* Stacked (narrow) the rail is a normal block, so the controls go back to
   reading left-to-right instead of as a tall column. */
@media (max-width: 879px) {
  .hbp-side-list {
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 4px;
  }
  .hbp-resultbar {
    grid-auto-flow: column;
    justify-content: space-between;
    align-items: center;
    width: 100%;
  }
}

.hbp-result-count {
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--hb-text-dim);
}

.hbp-clear {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--hb-border-strong);
  border-radius: var(--hb-radius-sm);
  background: transparent;
  color: var(--hb-text-dim);
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.hbp-clear:hover { border-color: var(--hb-cyan); color: var(--hb-cyan); }

/* === NO RESULTS === */
.hbp-empty {
  margin-top: 28px;
  padding: 36px 24px;
  text-align: center;
  border: 1px dashed var(--hb-border-strong);
  border-radius: var(--hb-radius);
  background: var(--hb-surface);
}

.hbp-empty-title {
  font-family: var(--hb-font-mono);
  font-size: 16px;
  color: var(--hb-text);
  margin-bottom: 8px;
}

.hbp-empty-body {
  font-size: 14px;
  color: var(--hb-text-muted);
  margin-bottom: 20px;
}

/* === CATEGORY GROUPS ===
   Each group carries its own accent, inherited by everything inside it (the
   card hover rule, the expanded-card border, the "Full story" toggle). Two
   19- and 17-entry groups sitting next to each other are the reason: a colour
   plus a numeral tells you which band you are scrolling through without
   having to scroll back up to the heading. */
.hbp-groups {
  display: grid;
  gap: clamp(44px, 5vw, 64px);
}

.hbp-group {
  --hbp-accent:      var(--hb-cyan);
  --hbp-accent-soft: var(--hb-cyan-soft);
  --hbp-accent-dim:  var(--hb-cyan-dim);
}

.hbp-group[data-group="flagship"],
.hbp-group[data-group="research"] {
  --hbp-accent:      var(--hb-amber);
  --hbp-accent-soft: var(--hb-amber-soft);
  --hbp-accent-dim:  var(--hbp-amber-dim);
}

.hbp-group[data-group="current"] {
  --hbp-accent:      var(--hbp-green);
  --hbp-accent-soft: var(--hbp-green-soft);
  --hbp-accent-dim:  var(--hbp-green-dim);
}

.hbp-group[data-group="open-source"] {
  --hbp-accent:      var(--hbp-violet);
  --hbp-accent-soft: var(--hbp-violet-soft);
  --hbp-accent-dim:  var(--hbp-violet-dim);
}

/* The sidebar already names every band and counts it, so the head here does not
   need to be a banner as well — a rule, a numeral and one line of blurb is
   enough, and it gives ~40px per band back to the entries. */
.hbp-group-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 10px;
  row-gap: 6px;
  margin-bottom: 18px;
  padding: 0 0 10px 10px;
  border-left: 3px solid var(--hbp-accent-dim);
  border-bottom: 1px solid var(--hb-border);
}

.hbp-group-index {
  font-family: var(--hb-font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--hbp-accent);
}

.hbp-group-title {
  grid-column: 2;
  font-family: var(--hb-font-sans);
  font-weight: 700;
  font-size: clamp(18px, 2.2vw, 21px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--hb-text);
}

.hbp-group-count {
  grid-column: 3;
  justify-self: end;
  padding: 4px 10px;
  border: 1px solid var(--hbp-accent-dim);
  border-radius: 999px;
  background: var(--hbp-accent-soft);
  font-family: var(--hb-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--hbp-accent);
}

.hbp-group-blurb {
  grid-column: 2 / -1;
  max-width: 88ch;
  font-size: 13px;
  line-height: 1.55;
  color: var(--hb-text-muted);
}

/* === CARDS ===
   Every tile renders the same five bands in the same order — head, provenance,
   description, tech + proof footer, "Full story" toggle — and the row stretches
   them to a common height. That, plus a line-clamped description, is what makes
   a 19-entry grid scannable instead of ragged. */
.hbp-cards {
  display: grid;
  gap: 14px;
  grid-template-columns: minmax(0, 1fr);
  align-items: stretch;
}

.hbp-card-item {
  min-width: 0;
  display: flex;
}

.hbp-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 11px;
  padding: 17px 18px;
  border: 1px solid var(--hb-border);
  border-radius: var(--hb-radius);
  background: var(--hb-surface);
  box-shadow: var(--hb-card-shadow);
  overflow-wrap: anywhere;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s var(--hb-ease), box-shadow 0.2s ease;
}

/* The tile fills whatever height its row settled on, so the footers of
   neighbouring cards line up instead of floating. */
.hbp-card-item > .hbp-card { flex: 1 1 auto; }

/* Group-accent hairline along the top edge — only on hover / open / target,
   so a resting grid stays quiet. */
.hbp-card::after {
  content: "";
  position: absolute;
  top: -1px;
  left: 16px;
  right: 16px;
  height: 2px;
  border-radius: 0 0 2px 2px;
  background: var(--hbp-accent, var(--hb-cyan));
  opacity: 0;
  transition: opacity 0.2s ease;
}

.hbp-card:hover {
  border-color: var(--hbp-accent-dim, var(--hb-cyan-dim));
  box-shadow: var(--hb-card-shadow-lift);
  transform: translateY(-3px);
}

.hbp-card:hover::after { opacity: 0.8; }

.hbp-card:target {
  border-color: var(--hbp-accent, var(--hb-cyan));
  box-shadow: 0 0 0 3px rgba(var(--hb-cyan-rgb), 0.14);
}

.hbp-card:target::after { opacity: 1; }

/* --- card visual: a small badge, never a banner — logo/illustration when
   real, else a fallback glyph by type. Sits beside the title, not above it. */
.hbp-card-badge {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--hb-border);
  border-radius: 9px;
  background: var(--hb-surface-2);
  overflow: hidden;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.hbp-card-badge img {
  width: 78%;
  height: 78%;
  object-fit: contain;
}

/* GitHub's mark where the proof is a repository — the source is the artifact,
   so its host is the honest logo for it. */
.hbp-card-badge[data-mark="github"] {
  color: var(--hb-text);
  background: var(--hb-surface-2);
}

.hbp-card:hover .hbp-card-badge[data-mark="github"] {
  color: var(--hb-cyan);
  border-color: var(--hb-cyan-dim);
}

.hbp-card-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.hbp-card-head-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.hbp-card-title {
  font-family: var(--hb-font-mono);
  font-weight: 600;
  font-size: 15.5px;
  line-height: 1.32;
  letter-spacing: -0.015em;
  color: var(--hb-text);
  min-width: 0;
}

.hbp-card-year {
  flex: 0 0 auto;
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--hb-text-muted);
}

/* --- provenance line: who owns it, and whether it is still alive --- */
.hbp-card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  color: var(--hb-text-muted);
}

.hbp-label {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border: 1px solid var(--hb-border);
  border-radius: 999px;
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--hb-text-muted);
  background: var(--hb-hairline);
}

.hbp-label-owned,
.hbp-label-co-authored {
  color: var(--hb-cyan);
  border-color: var(--hb-cyan-dim);
  background: var(--hb-cyan-soft);
}

/* Amber reads as "this one is not mine" at a glance. */
.hbp-label-upstream-contribution,
.hbp-label-maintained-fork {
  color: var(--hb-amber);
  border-color: var(--hbp-amber-dim);
  background: var(--hb-amber-soft);
}

/* Status is a lifecycle fact, not a category — so it is a coloured dot on a
   shared scale (live / done / winding down) rather than another pill competing
   with the ownership label next to it. */
.hbp-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--hb-text-muted);
}

.hbp-status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 16%, transparent);
}

.hbp-status[data-status="active"],
.hbp-status[data-status="maintained"] { color: var(--hbp-green); }

.hbp-status[data-status="stable"],
.hbp-status[data-status="published"],
.hbp-status[data-status="complete"] { color: var(--hb-text-dim); }

.hbp-status[data-status="dormant"],
.hbp-status[data-status="placeholder"] { color: var(--hb-amber); }

.hbp-status[data-status="archived"],
.hbp-status[data-status="retired"] { color: var(--hb-text-muted); }

/* --- the description: what was actually built, readable without a click. --- */
.hbp-card-desc {
  flex: 0 1 auto;
  font-size: 14px;
  line-height: 1.62;
  color: var(--hb-text-dim);
}

/* --- the summary used by the research detail pane: contribution + outcome,
   read as one paragraph, not a question-and-answer worksheet --- */
.hbp-card-summary {
  padding-top: 12px;
  border-top: 1px solid var(--hb-border);
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--hb-text-dim);
}

/* --- footer of a card: stack over proof.
   Two fixed bands rather than one wrapping row — a footer that sometimes
   wraps and sometimes does not is what made the old grid look ragged.
   No longer pinned to the bottom: the story paragraph now follows it in the
   same flow, so an auto top-margin here just shoved a gap between the
   description and the footer instead of sitting at the card's true end. Any
   row-height slack from a taller neighbour now lands after the last
   paragraph, where empty space reads as empty space rather than a seam. --- */
.hbp-card-foot {
  display: grid;
  justify-items: start;
  gap: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--hb-border);
}


.hbp-techs {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  min-width: 0;
}

.hbp-techs li {
  padding: 2px 7px;
  border: 1px solid var(--hb-border);
  border-radius: 5px;
  font-family: var(--hb-font-mono);
  font-size: 10.5px;
  white-space: nowrap;
  color: var(--hb-text-muted);
}

.hbp-proof {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  white-space: nowrap;
  color: var(--hb-cyan);
  border-bottom: 1px solid transparent;
}

.hbp-proof:hover { border-bottom-color: var(--hb-cyan-dim); }

.hbp-proof-none {
  color: var(--hb-text-muted);
  border-bottom: 0;
}

.hbp-proof-lock { flex: 0 0 auto; }

/* The full "why there is no link" sentence, shown once the card is open. */
.hbp-proof-reason {
  margin-top: 14px;
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--hb-text-muted);
}

/* type-led facts pulled up above the story in the research pane — tech stack
   for open-source, publication link for a paper. */
.hbp-techs-lead { margin: 0; }
.hbp-proof-lead { font-size: 13px; }

.hbp-card-story {
  padding-top: 14px;
  border-top: 1px solid var(--hb-border);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--hb-text-dim);
}

/* === FLAGSHIP EMPHASIS ===
   Two or three entries rendered at the same size as the other forty-three said
   they were interchangeable with them. This band keeps its own column count, so
   the page opens on large tiles and only then settles into the dense grid —
   which is also what stops a two-entry band from leaving a hole where the
   third column would have been. */
.hbp-group[data-group="flagship"] .hbp-card {
  border-color: var(--hb-border-strong);
  padding: 22px;
  gap: 13px;
}

.hbp-group[data-group="flagship"] .hbp-card::after { opacity: 0.4; }

.hbp-group[data-group="flagship"] .hbp-card-title {
  font-size: 20px;
  line-height: 1.25;
}

.hbp-group[data-group="flagship"] .hbp-card-badge {
  width: 46px;
  height: 46px;
  border-radius: 11px;
}

.hbp-group[data-group="flagship"] .hbp-card-mono { font-size: 16px; }

.hbp-group[data-group="flagship"] .hbp-card-desc {
  font-size: 14.5px;
}

/* === SUB-GROUPS ===
   A band that needs a second axis (org, ownership) gets one row of these.
   Deliberately lighter than a category head: a rule and a label, not a card,
   so the hierarchy stays readable as two levels rather than three. */
.hbp-sub + .hbp-sub { margin-top: clamp(26px, 3vw, 38px); }

.hbp-sub-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 14px;
  row-gap: 4px;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--hb-border);
}

.hbp-sub-title {
  grid-column: 1;
  font-family: var(--hb-font-mono);
  font-weight: 600;
  font-size: 14px;
  letter-spacing: 0.01em;
  color: var(--hb-text);
}

.hbp-sub-meta {
  grid-column: 1;
  grid-row: 2;
  font-family: var(--hb-font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--hb-text-muted);
}

.hbp-sub-sep { color: var(--hb-border-strong); margin: 0 6px; }

.hbp-sub-note {
  grid-column: 1 / -1;
  grid-row: 3;
  max-width: 76ch;
  margin-top: 4px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--hb-text-muted);
}

.hbp-sub-link {
  grid-column: 2;
  grid-row: 1 / span 2;
  justify-self: end;
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  white-space: nowrap;
  color: var(--hb-text-muted);
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.hbp-sub-link:hover {
  color: var(--hbp-accent, var(--hb-cyan));
  border-bottom-color: var(--hbp-accent-dim, var(--hb-cyan-dim));
}

/* === ROWS ===
   One entry per line. The whole summary is the toggle; the proof link is an
   icon in the corner, outside <details>, because a link inside a <summary> is
   a nested control that keyboards and screen readers both mishandle. */
.hbp-rows {
  display: grid;
  gap: 6px;
}

.hbp-row-item { min-width: 0; }

.hbp-row {
  position: relative;
  border: 1px solid var(--hb-border);
  border-radius: var(--hb-radius-sm);
  background: var(--hb-surface);
  transition: border-color 0.15s ease, background 0.15s ease;
}

.hbp-row:hover {
  border-color: var(--hbp-accent-dim, var(--hb-cyan-dim));
  background: var(--hb-surface-2);
}

.hbp-row:target {
  border-color: var(--hbp-accent, var(--hb-cyan));
  box-shadow: 0 0 0 3px rgba(var(--hb-cyan-rgb), 0.14);
}

.hbp-row-summary {
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr);
  column-gap: 10px;
  align-items: start;
  padding: 12px 46px 12px 12px;
  cursor: pointer;
  list-style: none;
}

.hbp-row-summary::-webkit-details-marker { display: none; }

.hbp-row-summary:focus-visible {
  outline: 2px solid var(--hb-cyan);
  outline-offset: -2px;
  border-radius: var(--hb-radius-sm);
}

.hbp-row-caret {
  width: 14px;
  height: 20px;
  position: relative;
  color: var(--hb-text-muted);
}

.hbp-row-caret::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 6px;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.18s var(--hb-ease);
}

.hbp-row-details[open] .hbp-row-caret::before { transform: rotate(45deg); }
.hbp-row:hover .hbp-row-caret { color: var(--hbp-accent, var(--hb-cyan)); }

.hbp-row-main { min-width: 0; }

.hbp-row-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.hbp-row-title {
  font-family: var(--hb-font-mono);
  font-weight: 600;
  font-size: 14px;
  line-height: 1.35;
  color: var(--hb-text);
}

/* Where it was published (or what it is written in) — the fact a reader wants
   first on a publication list, so it sits beside the title, not in a footer. */
.hbp-venue {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border: 1px solid var(--hb-border-strong);
  border-radius: 999px;
  font-family: var(--hb-font-mono);
  font-size: 10.5px;
  letter-spacing: 0.05em;
  white-space: nowrap;
  color: var(--hb-text-dim);
  background: var(--hb-hairline);
}

.hbp-venue[data-venue-type="peer-reviewed"],
.hbp-venue[data-venue-type="book"] {
  color: var(--hb-amber);
  border-color: var(--hbp-amber-dim);
  background: var(--hb-amber-soft);
}

.hbp-venue[data-venue-type="article"] {
  color: var(--hb-cyan);
  border-color: var(--hb-cyan-dim);
  background: var(--hb-cyan-soft);
}

.hbp-venue[data-venue-type="internal"] {
  color: var(--hb-text-muted);
  border-style: dashed;
}

.hbp-row-year {
  font-family: var(--hb-font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--hb-text-muted);
}

.hbp-row-desc {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  margin-top: 4px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--hb-text-dim);
}

.hbp-row-details[open] .hbp-row-desc {
  display: block;
  -webkit-line-clamp: unset;
  line-clamp: unset;
  overflow: visible;
}

.hbp-row-body {
  padding: 0 14px 14px 36px;
}

.hbp-row-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
}

.hbp-row-proof-text { margin-top: 6px; }

/* The corner link: icon only, so a row never reserves width for label text
   that runs from "Read on Medium" to "View merged pull request". */
.hbp-row-proof {
  position: absolute;
  top: 8px;
  right: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--hb-text-muted);
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

a.hbp-row-proof:hover {
  color: var(--hbp-accent, var(--hb-cyan));
  border-color: var(--hbp-accent-dim, var(--hb-cyan-dim));
  background: var(--hb-surface);
}

.hbp-row-proof-none { cursor: default; opacity: 0.5; }

@media (min-width: 720px) {
  /* Wide enough to put the description on the same line as the title block. */
  .hbp-row-summary { padding-right: 52px; }
}

/* === COLUMN COUNT ===
   auto-fit + a minimum tile width, not a fixed count per breakpoint. Two things
   fall out of that: the number of columns follows the archive column's real
   width (which a 244px sidebar changes, and a viewport media query cannot see),
   and a band holding fewer entries than there are tracks stretches to fill the
   row instead of leaving an empty cell where a card should be. */
/* min(..., 100%) is load-bearing: a bare minmax(255px, 1fr) is a hard floor, so
   on a 350px content column the 400px flagship track overflowed the page and
   took every other section sideways with it. */
.hbp-cards { grid-template-columns: repeat(auto-fit, minmax(min(255px, 100%), 1fr)); }

/* The flagship band keeps its own, larger minimum, so the page opens on big
   tiles before it settles into the dense grid. */
.hbp-group[data-group="flagship"] .hbp-cards:not(:has([data-shot="wide"], [data-shot="app"])) {
  grid-template-columns: repeat(auto-fit, minmax(min(400px, 100%), 1fr));
}

@supports (container-type: inline-size) {
  .hbp-main {
    container-type: inline-size;
    container-name: archive;
  }
}

/* === REDUCED MOTION === */
@media (prefers-reduced-motion: reduce) {
  .hbp-card:hover { transform: none; }
}

/* --- upstream reach ---
   Star count of the project a patch landed in, never of Tai's own repos. */
/* Rounded rect, not a pill: the repo names run to 38 characters, so the text
   wraps — and a 999px radius then cuts into the first and last words on each
   line. Also min-width:0 so a long name can break instead of forcing the chip
   wider than its card. */
.hbp-host {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px 8px;
  min-width: 0;
  max-width: 100%;
  padding: 5px 9px;
  border: 1px solid var(--hb-border);
  border-radius: var(--hb-radius-sm);
  background: var(--hb-surface-2);
  font-family: var(--hb-font-mono);
  font-size: 11px;
  line-height: 1.4;
}

.hbp-host-label {
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--hb-text-muted);
}

.hbp-host-repo {
  color: var(--hb-text-dim);
  border-bottom: 1px solid transparent;
  overflow-wrap: anywhere;
}

.hbp-host-repo:hover { color: var(--hb-cyan); border-bottom-color: var(--hb-cyan-dim); }

.hbp-host-stars {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--hb-amber);
  font-weight: 600;
}

.hbp-row-body .hbp-host { margin-top: 14px; }

/* --- live product thumbnail, on the tile ---
   Small on purpose: it is evidence the thing runs, not the point of the card.
   A 16:9 strip at tile width keeps the card's five bands intact and the grid
   rows still line up. */
.hbp-shot {
  margin: auto 0 0;
  overflow: hidden;
  border-radius: var(--hb-radius-sm);
  border: 1px solid var(--hb-border);
  transition: border-color 0.15s ease;
}

.hbp-shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 21 / 9;
  object-fit: cover;
  object-position: top center;
  transition: transform 0.3s var(--hb-ease);
}

.hbp-card:hover .hbp-shot { border-color: var(--hbp-accent-dim, var(--hb-cyan-dim)); }
.hbp-card:hover .hbp-shot img { transform: scale(1.03); }

/* In a row the tile does not exist, so it stays in the opened body. */
.hbp-row-body .hbp-shot { max-width: 460px; margin-bottom: 16px; }
.hbp-row-body .hbp-shot img { aspect-ratio: 16 / 9; }

/* An App Store listing is portrait; cropping it to a 21:9 strip would show a
   sliver of a phone. Letterbox it on the card's own surface instead. */
.hbp-shot[data-kind="app"] {
  background: var(--hb-surface-2);
}

.hbp-shot[data-kind="app"] img {
  object-fit: contain;
  object-position: center;
}

/* ============================================================
   BENTO — THREE TILE TYPES
   wide  a landscape web capture. Two columns, image across the
         top at 16:9, which is the aspect the captures are shot
         at, so nothing is cropped.
   app   a portrait App Store capture. One column, image at 4:5
         and anchored to the top so the icon, name and rating
         stay in frame — a phone letterboxed inside a 16:9 band
         wasted most of the tile.
   none  nothing public to show. Compact, and where the entry
         says why, that line sits in the image's place.
   ============================================================ */
@supports (container-type: inline-size) {
  @container archive (min-width: 800px) {
    .hbp-cards { grid-auto-flow: dense; }
    .hbp-cards:has([data-shot="wide"], [data-shot="app"]) {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    .hbp-card-item[data-shot="wide"] { grid-column: span 2; }
  }
}

/* Tiles size to their own content — a 272px capture must not set the height for
   a tile that has none. Outside the container query: at two columns the
   mismatch is just as visible. */
.hbp-cards:has([data-shot="wide"], [data-shot="app"]) { align-items: stretch; }

.hbp-shot {
  margin: 0 0 2px;
  order: -1;
}

.hbp-shot img {
  aspect-ratio: 16 / 9;
  max-height: 272px;
  object-fit: cover;
  object-position: top center;
}

/* Portrait source in a portrait frame: no letterboxing, and cropping from the
   top keeps the app icon, name and star rating visible. */
.hbp-card-item[data-shot="app"] .hbp-shot img {
  aspect-ratio: 5 / 4;
  max-height: 272px;
  object-fit: cover;
  object-position: top center;
}

/* --- the "nothing to show" tile --- */
.hbp-noshot {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: start;
  margin-top: auto;
  padding: 4px 9px 4px 8px;
  border: 1px dashed var(--hb-border-strong);
  border-radius: 999px;
  font-family: var(--hb-font-mono);
  font-size: 10.5px;
  line-height: 1.3;
  white-space: nowrap;
  color: var(--hb-text-muted);
}

.hbp-noshot svg { flex: 0 0 auto; }

/* Employer attribution on a card that sits outside its org sub-group. */
.hbp-card-org {
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--hb-text-muted);
}

.hbp-card-org::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: middle;
  margin-right: 7px;
  opacity: 0.6;
}

/* Redundant where the sub-group header already says it. */
[data-subgroup] .hbp-card-org { display: none; }
