/* Habitus Mind Blog — listing + post detail.
   Loads after /style.css (the marketing site's Mercury tokens/components:
   .card, .block, .btn-cta, .answer, header/footer/WhatsApp FAB are all
   already defined there and reused as-is). This file only adds what the
   blog needs on top: category pills, the post grid, article typography,
   and the author card.

   v1 ships text-only — no photography (see blog/build.mjs's comment and
   the chat decision: real licensed photos weren't sourced for six posts
   under this pass, and a generated/gradient stand-in was ruled out by
   this project's own imagery standard). Cards read as editorial text
   cards, not broken image placeholders. */

/* ---------------------------------------------------------------
   Listing — rewritten 2026-09-20 to Design1.pdf section 4a (and the
   phone stack in 6a). The three complaints it fixes, in order:
   a masthead that showed no posts, three card densities in one
   featured row, and twelve full-height cards with no pagination.
   --------------------------------------------------------------- */

.eyebrow {
  color: var(--color-accent);
  font: 400 13px/1.3 var(--font-mono);
  letter-spacing: 0.14em;
  margin: 0 0 var(--space-12);
  text-transform: uppercase;
}

.sr-only {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* ---------- Masthead: one band, not a first screen ---------- */

.blog-masthead {
  align-items: end;
  display: grid;
  gap: var(--space-16) var(--space-56);
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  margin: 0 auto;
  max-width: var(--page-max-width);
  padding: var(--space-56) var(--space-24) var(--space-32);
}

.blog-masthead h1 {
  font-size: clamp(30px, 3.4vw, 46px);
  font-weight: 480;
  letter-spacing: -0.02em;
  line-height: 1.1;
  margin: 0;
}

.blog-masthead-sub {
  color: var(--color-ink-soft);
  font-size: 15px;
  line-height: 1.6;
  margin: 0;
  max-width: 44ch;
}

.blog-list {
  margin: 0 auto;
  max-width: var(--page-max-width);
  padding: 0 var(--space-24) var(--space-112);
}

/* ---------- Filters: underline tabs + search ---------- */

.filter-bar {
  align-items: center;
  border-bottom: 1px solid rgba(255, 60, 74, 0.14);
  display: flex;
  gap: var(--space-24);
  justify-content: space-between;
  margin-bottom: var(--space-40);
}

/* Horizontal scroll with a fade cue rather than chips that clip at the
   right edge with nothing to say there is more. */
.filter-row {
  display: flex;
  gap: var(--space-24);
  margin-bottom: -1px;
  overflow-x: auto;
  /* The fade lands on this padding when the row fits, and on the last
     tab only when there is genuinely more to scroll to. Without it the
     mask clips the final category on a row that does not overflow. */
  padding-right: 28px;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
          mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.filter-row::-webkit-scrollbar { display: none; }

.filter-tab {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--color-ink-soft);
  cursor: pointer;
  font: 400 16px var(--font-body);
  padding: var(--space-12) 0;
  white-space: nowrap;
}
.filter-tab:hover { color: var(--color-ink); }
.filter-tab.is-active { border-bottom-color: var(--color-accent); color: var(--color-ink); }
.filter-tab:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.blog-search input {
  background: var(--color-surface-2);
  border: 1px solid rgba(255, 60, 74, 0.2);
  border-radius: var(--radius-inputs);
  color: var(--color-ink);
  font: 400 14px var(--font-body);
  min-height: 38px;
  padding: 0 var(--space-16);
  width: 180px;
}
.blog-search input::placeholder { color: var(--color-ink-soft); }
.blog-search input:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ---------- Featured: a 50/50 editorial row ---------- */

.featured-row {
  align-items: center;
  border-bottom: 1px solid rgba(255, 60, 74, 0.14);
  color: inherit;
  display: grid;
  gap: clamp(var(--space-24), 4vw, var(--space-56));
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  padding-bottom: var(--space-56);
}
.featured-row:hover { text-decoration: none; }

.featured-row-media { border-radius: var(--radius-cards); }
.featured-row-media img { transition: transform 500ms cubic-bezier(0.16, 1, 0.3, 1); }
.featured-row:hover .featured-row-media img { transform: scale(1.02); }

.featured-row h2 {
  font-size: clamp(24px, 2.6vw, 36px);
  font-weight: 480;
  letter-spacing: -0.02em;
  line-height: 1.12;
  margin: var(--space-12) 0 var(--space-16);
}

.featured-row .excerpt { margin-bottom: var(--space-16); }

/* The card version splits category left / read time right. On the
   featured row the three parts read as one line, so it is plain inline
   text — flex squeezed the middle item and wrapped it mid-phrase at
   phone widths. */
.featured-row .meta-line { display: block; line-height: 1.5; }

/* ---------- The one meta voice ---------- */

.meta-line {
  align-items: baseline;
  color: var(--color-accent);
  display: flex;
  font: 400 12px/1 var(--font-mono);
  justify-content: space-between;
  letter-spacing: 0.12em;
  margin: 0;
  text-transform: uppercase;
}
.meta-time { color: var(--color-ink-soft); }
.meta-muted { color: var(--color-ink-soft); font-size: 14px; }

.excerpt { color: var(--color-ink-soft); font-size: 16px; line-height: 1.6; }

.read-more { color: var(--color-accent); font: 400 15px var(--font-body); }
.featured-row:hover .read-more { text-decoration: underline; text-underline-offset: 3px; }

/* ---------- Latest ---------- */

.latest-head {
  align-items: baseline;
  display: flex;
  justify-content: space-between;
  margin: var(--space-56) 0 var(--space-32);
}

.latest-heading { font-size: 22px; font-weight: 480; letter-spacing: -0.01em; margin: 0; }
.latest-note { color: var(--color-ink-soft); font: 400 12px var(--font-mono); letter-spacing: 0.08em; margin: 0; }

.post-grid {
  display: grid;
  gap: var(--space-40) var(--space-32);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 900px) { .post-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* No border, no chip, no watermark: one weight for every card, which is
   what makes the featured row above read as featured. */
.post-card {
  color: inherit;
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}
.post-card:hover { text-decoration: none; }
.post-card .cover-media { border-radius: var(--radius-cards); }
/* The hover zoom uses the independent `scale` property rather than
   `transform`, so the scroll drift below can own `translate` without the
   two fighting over one property (an animation on transform would beat
   the hover rule outright and kill the zoom). */
.post-card .cover-media img { transition: scale 500ms cubic-bezier(0.16, 1, 0.3, 1); }
.post-card:hover .cover-media img { scale: 1.03; }

.post-card h3 {
  font-size: 19px;
  font-weight: 520;
  letter-spacing: -0.01em;
  line-height: 1.3;
  margin: 0;
}
.post-card:hover h3 { color: var(--color-accent); }

/* Clamped by lines, not by a server-side character cut — the old
   excerpts ended mid-word because they were truncated before render. */
.post-card .excerpt {
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  display: -webkit-box;
  font-size: 15px;
  margin: 0;
  overflow: hidden;
}

/* Every one of these sets its own `display`, which beats the UA
   stylesheet's [hidden] rule at equal specificity — so hiding one from
   JS needs the override spelled out here or the element stays on screen.
   .featured-row and .latest-head joined the list when the category
   filter started reaching the featured post. */
.post-grid[hidden],
.post-card[hidden],
.featured-row[hidden],
.latest-head[hidden] { display: none; }

.show-more-row { display: flex; justify-content: center; margin-top: var(--space-56); }

.show-more {
  background: none;
  border: 1px solid var(--color-ink);
  border-radius: var(--radius-buttons);
  color: var(--color-ink);
  cursor: pointer;
  font: 400 16px var(--font-body);
  min-height: 48px;
  padding: 0 var(--space-32);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.show-more:hover { background: rgba(255, 60, 74, 0.1); border-color: var(--color-accent); }
.show-more:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }

.empty-state {
  color: var(--color-ink-soft);
  padding: var(--space-56) 0;
  text-align: center;
}

/* ---------- Phone (6a) ----------
   Two-line header with the featured post on the first screen, and
   posts as 88px list rows rather than 400px blocks. */

@media (max-width: 760px) {
  .blog-masthead { grid-template-columns: 1fr; padding: var(--space-40) var(--space-16) var(--space-24); }
  .blog-list { padding-inline: var(--space-16); }
  .filter-bar { gap: var(--space-12); }
  .blog-search input { width: 110px; }

  .featured-row { grid-template-columns: 1fr; gap: var(--space-16); padding-bottom: var(--space-32); }
  .featured-row h2 { font-size: 22px; margin: var(--space-8) 0 var(--space-12); }
  /* Three lines on a phone: the server already clipped this to a word
     boundary, so the clamp is only a safety net and should not fire on
     a normal excerpt. */
  .featured-row .excerpt { -webkit-box-orient: vertical; -webkit-line-clamp: 3; display: -webkit-box; overflow: hidden; }

  .latest-head { margin: var(--space-32) 0 var(--space-8); }

  .post-grid { display: block; }
  .post-card {
    align-items: center;
    border-top: 1px solid rgba(255, 60, 74, 0.14);
    column-gap: var(--space-16);
    display: grid;
    grid-template-areas: "meta thumb" "title thumb";
    grid-template-columns: minmax(0, 1fr) 96px;
    min-height: 88px;
    padding: var(--space-16) 0;
    row-gap: var(--space-4);
  }
  .post-card .cover-media { grid-area: thumb; }
  .post-card .meta-line { font-size: 11px; grid-area: meta; gap: var(--space-8); justify-content: flex-start; letter-spacing: 0.08em; }
  .post-card .meta-time::before { content: "\00b7\00a0"; }
  .post-card h3 { grid-area: title; font-size: 16px; }
  .post-card .excerpt { display: none; }

  .show-more-row { margin-top: var(--space-32); }
  .show-more { width: 100%; }
}

/* ---------- Carried over for the POST page only ----------
   The listing above dropped chips, author rows and pills (4a). The post
   detail page still uses them: a byline under an article title is not
   the same object as a chip on a card, and the share row is genuinely a
   set of pills. Kept here, scoped by comment rather than by selector so
   they are easy to find if the post page is redesigned next. */

.tag {
  align-items: center;
  background: rgba(255, 60, 74, 0.12);
  border-radius: var(--radius-tags);
  color: var(--color-accent-soft);
  display: inline-flex;
  font: 400 13px var(--font-body);
  height: 28px;
  padding: 0 var(--space-12);
  width: fit-content;
}

.post-meta { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-8); }
.post-meta .avatar-chip { width: 32px; height: 32px; font-size: 12px; }
.meta-name { color: var(--color-ink); font-size: 14px; }
a.meta-name, .author-name a { text-decoration: none; }
a.meta-name:hover, .author-name a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* The author's photo sits over the initials (functions/_lib/blog-render.js
   avatarChip), inside the red .author-ring from /style.css. CSP forbids an inline onerror, so the initials underneath
   ARE the fallback. The img's alt text is transparent so it can't print
   over them, and in Chrome, which paints a torn-image glyph on a failed
   load, the ::after (rendered only for a broken img, never a loaded one)
   repeats the initials on top of the glyph. */
.avatar-chip.has-photo { position: relative; --ring-gap: 2px; }
.post-meta .avatar-chip.has-photo { margin: 0 var(--space-4); }
.avatar-chip-lg.has-photo { --ring-gap: 3px; margin: 5px; }
.avatar-chip.has-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  font: inherit;
  color: transparent;
}
.avatar-chip.has-photo img::after {
  content: attr(data-initials);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-surface-2);
  color: var(--color-accent);
}
.meta-dot { color: var(--color-ink-soft); }

.filter-pill {
  align-items: center;
  background: none;
  border: 1px solid rgba(255, 60, 74, 0.25);
  border-radius: var(--radius-tags);
  color: var(--color-ink-soft);
  cursor: pointer;
  display: inline-flex;
  font: 400 14px var(--font-body);
  height: 38px;
  padding: 0 var(--space-20);
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.filter-pill:hover { background: rgba(255, 60, 74, 0.08); border-color: var(--color-accent); color: var(--color-ink); text-decoration: none; }
.filter-pill:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ---------- Post detail ----------
   .post-detail is a full-width .block — only the header text and the
   article column below get their own max-width, so the hero image can
   run the full block width while the prose stays a readable measure
   (design/Habitusmind blog page_v2). */

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  font-size: 14px;
  color: var(--color-ink-soft);
  margin-bottom: var(--space-24);
}
.breadcrumb a { color: var(--color-ink-soft); }
.breadcrumb a:hover { color: var(--color-ink); }
.breadcrumb [aria-current="page"] { color: var(--color-ink); }

.post-header { max-width: 46rem; }

.post-header h1 {
  margin-top: var(--space-16);
  font-size: clamp(32px, 5vw, 48px);
}

.post-meta-lg { margin-top: var(--space-20); }
.post-meta-lg .avatar-chip { width: 36px; height: 36px; }

.post-share {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-left: var(--space-16);
}
@media (max-width: 560px) { .post-share { margin-left: 0; } }

/* ---------- Article + "On this page" sidebar ---------- */
.post-layout {
  display: flex;
  align-items: flex-start;
  gap: var(--space-40);
  flex-wrap: wrap;
  margin-top: var(--space-32);
}
.post-article { flex: 3 1 500px; min-width: 0; max-width: 46rem; }

.post-toc {
  flex: 1 1 200px;
  min-width: 200px;
  position: sticky;
  top: 96px;
}
.post-toc-label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  margin-bottom: var(--space-12);
}
.post-toc nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  border-left: 2px solid var(--color-line-strong);
  padding-left: var(--space-16);
}
.post-toc a { font-size: 14px; color: var(--color-ink-soft); line-height: 1.4; }
.post-toc a:hover { color: var(--color-ink); }
@media (max-width: 900px) {
  .post-toc { position: static; }
}

.post-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-20);
}
.post-body h2 { font-size: 26px; margin-top: var(--space-16); }
.post-body p { color: var(--color-ink-soft); font-size: 17px; line-height: 1.7; max-width: 100%; }
.post-body ul { margin: 0; padding-left: 1.25em; color: var(--color-ink-soft); font-size: 17px; line-height: 1.7; }
.post-body li { margin-bottom: var(--space-8); }
.post-body li::marker { color: var(--color-accent); }

/* Tables (SERP formats D, 2026-10-14). blog-sanitize.js allows table,
   caption, thead, tbody, tr, th and td; blog-render.js's wrapTables() puts
   each table in .post-table, which scrolls sideways on a phone so the page
   never does. Dark canvas: hairlines in the accent tint, numbers aligned. */
.post-table {
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.post-table:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.post-body table {
  border-collapse: collapse;
  color: var(--color-ink-soft);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
  min-width: 100%;
}
.post-body caption {
  caption-side: top;
  color: var(--color-ink);
  font-weight: 480;
  padding-bottom: var(--space-12);
  text-align: left;
}
.post-body th,
.post-body td {
  border-bottom: 1px solid rgba(255, 60, 74, 0.18);
  padding: var(--space-12) var(--space-16) var(--space-12) 0;
  text-align: left;
  vertical-align: top;
}
.post-body th:last-child,
.post-body td:last-child { padding-right: 0; }
.post-body thead th { border-bottom-color: var(--color-line-strong); color: var(--color-ink); font-weight: 480; white-space: nowrap; }
.post-body tbody th { color: var(--color-ink); font-weight: 480; }

.post-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-top: var(--space-40);
  padding-top: var(--space-24);
  border-top: 1px solid rgba(255, 60, 74, 0.12);
}
.post-tag {
  font-size: 13px;
  color: var(--color-ink-soft);
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-tags);
  padding: var(--space-4) var(--space-12);
}

.author-card {
  display: flex;
  gap: var(--space-16);
  align-items: flex-start;
  margin-top: var(--space-40);
}
.avatar-chip-lg { width: 48px; height: 48px; font-size: 15px; flex-shrink: 0; }
.author-name { font: 480 16px var(--font-display); }
.author-role { font-size: 13px; color: var(--color-accent-soft); margin-top: 2px; }
.author-bio { font-size: 14px; color: var(--color-ink-soft); line-height: 1.6; margin-top: var(--space-8); }

.related-posts h2 { margin-bottom: var(--space-24); }

/* ---------- Rich content (WYSIWYG editor output) ----------
   The editor can now emit headings, ordered lists, quotes, code, rules,
   links, and images/figures — everything functions/_lib/blog-sanitize.js
   allows. These rules cover the tags the earlier markdown-only version
   never produced, so a post written in the visual editor renders as
   deliberately as the hand-written ones did. */

.post-body h3 { font-size: 21px; margin-top: var(--space-16); letter-spacing: normal; }
.post-body h4 { font-size: 18px; margin-top: var(--space-16); letter-spacing: normal; }

.post-body ol {
  margin: 0;
  padding-left: 1.25em;
  color: var(--color-ink-soft);
  font-size: 17px;
  line-height: 1.7;
}
.post-body ol li { margin-bottom: var(--space-8); }
.post-body ol li::marker { color: var(--color-accent); }

.post-body a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.post-body a:hover { color: var(--color-ink); }

.post-body strong { color: var(--color-ink); font-weight: 530; }
.post-body em { font-style: italic; }
.post-body u { text-underline-offset: 2px; }
.post-body s { opacity: 0.7; }

.post-body blockquote {
  margin: 0;
  padding: var(--space-4) 0 var(--space-4) var(--space-24);
  border-left: 2px solid var(--color-accent);
  color: var(--color-ink);
  font-size: 19px;
  line-height: 1.6;
  font-style: italic;
}

/* A TL;DR / key-takeaways callout is the same blockquote markup (a
   bold lead-in line + a bullet list) rather than a new tag — nothing
   outside the allowlist in functions/_lib/blog-sanitize.js. The quote's
   italic is right for a pulled sentence but wrong for a scannable list,
   so it's turned off just for the list inside one. This is also the
   shape answer engines and featured snippets tend to lift directly
   (GEO/AEO): a short labelled list right under the opening claim. */
.post-body blockquote p { font-style: normal; margin: 0 0 var(--space-8); }
.post-body blockquote ul,
.post-body blockquote ol {
  font-style: normal;
  margin: 0;
}
.post-body blockquote li { margin-bottom: var(--space-4); }

.post-body code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.88em;
  padding: 0.12em 0.4em;
  border-radius: var(--radius-default);
  background: var(--color-surface-2);
  color: var(--color-ink);
}

.post-body pre {
  margin: 0;
  padding: var(--space-20);
  border-radius: var(--radius-cards-inner);
  border: 1px solid var(--color-line-strong);
  background: var(--color-card);
  /* Wide code scrolls inside its own box — the page body must never
     scroll horizontally (see DESIGN.md's layout guidelines). */
  overflow-x: auto;
  font-size: 14px;
  line-height: 1.6;
}
.post-body pre code { background: none; padding: 0; font-size: inherit; }

.post-body hr {
  border: none;
  border-top: 1px solid var(--color-line-strong);
  margin: var(--space-16) 0;
}

/* Images. width is set inline as a percentage by the editor's resize
   control; max-width keeps that honest on narrow screens, and height:auto
   preserves the aspect ratio so nothing is squashed. */
.post-body img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-cards-inner);
  display: inline-block;
  vertical-align: middle;
}

.post-body figure { margin: var(--space-8) 0; }

.post-body figcaption {
  margin-top: var(--space-12);
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-ink-soft);
}

/* An image alone in a paragraph shouldn't inherit the body copy's
   bottom spacing twice over. */
.post-body p:has(> img:only-child) { line-height: 0; }

/* ---------- Cover images (design/Habitusmind blog page_v2) ----------
   ONE uploaded image per post, reused at four crops. The box sets the
   aspect ratio and the <img> is object-fit:cover'd into it, so the
   centre of the image survives everywhere and an author uploads once
   instead of four times. aspect-ratio also reserves the space before
   the image loads, so there's no layout shift (DESIGN.md's performance
   rule) without hardcoding pixel heights. */

.cover-media {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius-cards-inner);
  background: var(--color-surface-2);
}

/* THE WATERMARK IS BURNED INTO THE STORED IMAGE, not drawn here.
   Design1.pdf 4a asks for covers with no watermark; the "Habitus Mind"
   lockup a reader sees on the older covers is composited into the WebP
   bytes in D1, so no markup change removes it. Measured across them, it
   is a black bar in the bottom ~16% of a 1600x900 frame.

   WHERE THE CROP IS NEEDED, and where it is not:
   the post banner is 21:8, and object-fit:cover on a 16:9 source
   already discards everything below ~84% of the height — the bar is
   gone before any rule of ours runs. The card ratios (16:10, 16:9) are
   equal to or narrower than the source, so cover trims the SIDES and
   the bar survives. So the over-size is scoped to the cards only.
   Applying it to the banner as well cost 22% of a cover that never had
   a watermark in the first place (post 13's, generated 2026-09-20).

   Still a cover, not a cure: it costs 22% of the frame on the cards and
   it keeps cropping images that never needed it. THE REAL FIX IS
   RE-UPLOADING the older covers without the lockup in /blog-admin/,
   after which these two rules should go. */
.cover-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.post-card .cover-media img,
.featured-row-media img {
  inset: 0 0 auto 0;
  height: 122%;
  object-position: center top;
}

/* The post header banner is wider than any card and sits full-column. */
.post-hero {
  margin: var(--space-32) 0 var(--space-40);
  border-radius: var(--radius-cards);
}

/* PER-COVER FRAMING, post 20 (how-to-reconnect-with-old-contacts,
   2026-09-29). Its generated cover put the phone and the name card low
   in the frame (y 442-682 of 752), below the 82% line the 122% card
   rule above keeps, and below the 21:8 banner's centre band. It has no
   watermark, so its cards drop the over-size and anchor left of centre
   (18% keeps the phone's left end in the narrow 4:3 related card), and
   its banner anchors low (85% shows y 204-716). Keyed on the permanent
   slug, never on /blog-image/<id>, which differs between local and
   production. Browsers without :has() get the centred banner. Remove
   this block if the cover is ever replaced by a centred one. */
a.post-card[href$="/how-to-reconnect-with-old-contacts"] .cover-media img,
a.featured-row[href$="/how-to-reconnect-with-old-contacts"] .featured-row-media img {
  inset: 0;
  height: 100%;
  object-position: 18% 50%;
}
html:has(link[rel="canonical"][href$="/how-to-reconnect-with-old-contacts"]) .post-hero img {
  object-position: 50% 85%;
}

/* PER-COVER FRAMING, post 21 (how-numerology-compatibility-is-calculated,
   2026-09-30). Its generated cover puts the two cups and the saucer low in
   the frame (cup rims y 223, cup feet y 599, saucer bottom y 696 of 752),
   so the 122% card over-size, which keeps only the top ~82%, would cut the
   saucer off. No watermark, so the cards drop the over-size and centre
   (x 289-1037 of 1344 stays inside even the 4:3 related card, whose
   centred window is x 170-1173), and the 21:8 banner anchors low (80% shows
   y 192-704). Keyed on the permanent slug, like post 20's block above.
   Remove this block if the cover is ever replaced by a centred one. */
a.post-card[href$="/how-numerology-compatibility-is-calculated"] .cover-media img,
a.featured-row[href$="/how-numerology-compatibility-is-calculated"] .featured-row-media img {
  inset: 0;
  height: 100%;
  object-position: 50% 50%;
}
html:has(link[rel="canonical"][href$="/how-numerology-compatibility-is-calculated"]) .post-hero img {
  object-position: 50% 80%;
}

/* PER-COVER FRAMING, post 22 (how-much-protein-do-i-need-a-day, 2026-10-01).
   Its generated cover fills the frame: the egg basket and the banana-leaf
   tempeh sit in the top 30% (y 30-290 of 752) and the tray of ikan kembung
   runs from y 270 to the bottom edge. It has no watermark, so the cards drop
   the 122% over-size, which would keep only the top ~82% and cut the front
   row of fish, and centre. The 21:8 banner shows 512 of the 752 px, so it
   anchors high (30% shows y 72-584) to keep the eggs and the first two rows
   of fish together. Keyed on the permanent slug, like post 20's and 21's.
   Remove this block if the cover is ever replaced by a centred one. */
a.post-card[href$="/how-much-protein-do-i-need-a-day"] .cover-media img,
a.featured-row[href$="/how-much-protein-do-i-need-a-day"] .featured-row-media img {
  inset: 0;
  height: 100%;
  object-position: 50% 50%;
}
html:has(link[rel="canonical"][href$="/how-much-protein-do-i-need-a-day"]) .post-hero img {
  object-position: 50% 30%;
}

/* PER-COVER FRAMING, post 23 (follow-up-message-after-meeting-someone,
   2026-10-02). Its generated cover groups the glass of teh, the plate of
   kuih, the two name cards, the lanyard and the phone at y 232-643 of 752,
   so the 122% card over-size, which keeps only the top ~82%, would cut the
   lanyard and the bottom card. No watermark, so the cards drop the
   over-size and centre (x 192-1137 of 1344, inside the 4:3 related card's
   centred window x 170-1173), and the 21:8 banner anchors low (70% shows
   y 168-680). Keyed on the permanent slug, like posts 20 to 22. Remove this
   block if the cover is ever replaced by a centred one. */
a.post-card[href$="/follow-up-message-after-meeting-someone"] .cover-media img,
a.featured-row[href$="/follow-up-message-after-meeting-someone"] .featured-row-media img {
  inset: 0;
  height: 100%;
  object-position: 50% 50%;
}
html:has(link[rel="canonical"][href$="/follow-up-message-after-meeting-someone"]) .post-hero img {
  object-position: 50% 70%;
}

/* PER-COVER FRAMING, post 24 (how-to-calculate-your-personal-month-number,
   2026-10-03). Its generated cover is a top-down flat lay: the calendar pad
   runs y 17-709 of 752, the glass of teh y 22-311 and the fan of month tabs
   y 332-674, so the 122% card over-size, which keeps only the top ~82%,
   would clip the bottom of the fan. No watermark, so the cards drop the
   over-size and centre. The 21:8 banner keeps the default centre (y 120-632:
   the glass, the dots and all twelve tabs), so it needs no rule. Keyed on
   the permanent slug, like posts 20 to 23. Remove this block if the cover is
   ever replaced by a centred one. */
a.post-card[href$="/how-to-calculate-your-personal-month-number"] .cover-media img,
a.featured-row[href$="/how-to-calculate-your-personal-month-number"] .featured-row-media img {
  inset: 0;
  height: 100%;
  object-position: 50% 50%;
}

/* PER-COVER FRAMING, post 25 (weight-loss-challenge-with-friends,
   2026-10-08). Its generated cover keeps the bathroom scale and the half
   ring of slippers at y 165-593 and x 332-993 of 752 x 1344, on plain
   terrazzo, so the 122% card over-size, which keeps only the top ~82%,
   would sit the group low against the card's bottom edge. No watermark, so
   the cards drop the over-size and centre. The 21:8 banner keeps the
   default centre (y 120-632, the whole group), so it needs no rule. Keyed
   on the permanent slug, like posts 20 to 24. Remove this block if the
   cover is ever replaced. */
a.post-card[href$="/weight-loss-challenge-with-friends"] .cover-media img,
a.featured-row[href$="/weight-loss-challenge-with-friends"] .featured-row-media img {
  inset: 0;
  height: 100%;
  object-position: 50% 50%;
}

/* Brand lockup, bottom-right of a cover — the v2 design's own device
   for making a shared or screenshotted card still read as ours. */
.cover-brand {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  padding: 6px 13px 6px 8px;
  border-radius: var(--radius-tags);
  background: rgba(5, 4, 4, 0.58);
  border: 1px solid rgba(244, 236, 236, 0.14);
  backdrop-filter: blur(6px);
  pointer-events: none;
  font: 500 11px/1 var(--font-body);
  letter-spacing: 0.06em;
  color: var(--color-ink);
  white-space: nowrap;
}
.cover-brand img { position: static; width: 18px; height: 18px; border-radius: 5px; flex: none; }

/* Small cards get the mark alone — the wordmark is unreadable at card
   scale and just becomes noise over the image. */
.cover-brand-mark {
  right: 8px;
  bottom: 8px;
  padding: 5px;
  gap: 0;
}
.cover-brand-mark img { width: 14px; height: 14px; border-radius: 4px; }

.cover-pill {
  position: absolute;
  left: 14px;
  top: 14px;
  z-index: 2;
  font: 500 10px/1 var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 6px 12px;
  border-radius: var(--radius-tags);
  background: var(--color-accent);
  color: var(--color-white);
  white-space: nowrap;
}

/* With a cover present the card's own top padding would double the gap
   under the image, so the media sits flush to the card's inner edge. */
.post-card.has-cover .cover-media { margin-bottom: var(--space-4); }

@media (max-width: 760px) {
  /* 21:8 is a letterbox on a phone — barely 50px tall at 375px wide.
     The banner relaxes toward the card ratio so it still reads as an
     image rather than a stripe. */
  .post-hero { aspect-ratio: 3 / 2 !important; }
  .cover-brand { right: 8px; bottom: 8px; padding: 5px 10px 5px 6px; font-size: 10px; }
  .cover-brand img { width: 14px; height: 14px; }
}

/* ---------- Covers drift, categories carry colour (redesign 2026 R1c) ----------
   Brief section 2, motion 5. The cover moves about 16px across its whole
   pass through the viewport — transform-family only (the `translate`
   property), so it composes with the hover `scale` and never lays out.
   Scroll-linked through `animation-timeline: view()`, with no JavaScript
   and no observer; browsers without it simply get a still cover, which
   is the graceful end of this effect rather than a broken one.

   The cards already over-size their image (height: 122%, anchored top,
   to hide a burned-in watermark), so an 8px drift each way stays inside
   the frame and never reveals an edge. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .post-card .cover-media img,
    .featured-row .cover-media img {
      animation: blog-cover-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes blog-cover-drift {
      from { translate: 0 -8px; }
      to   { translate: 0 8px; }
    }
  }
}

/* Category colour. Keyed on data-cat-key, which is the ENGLISH category
   on both language sites (see postOut in functions/_lib/blog-render.js).
   Every value stays inside the brand's warm range — crimson through
   amber, mixed in oklab — because blue is reserved for anything that
   costs an AI Credit and green for success and WhatsApp. A category not
   listed here keeps the neutral label it has always had, so adding one
   in the CMS degrades to plain rather than to a broken colour. Both
   tokens are redefined for the light theme, so both themes follow. */
[data-cat-key="Personal CRM"] .meta-cat { color: var(--hm-accent-ink); }
[data-cat-key="Numerology"] .meta-cat { color: color-mix(in oklab, var(--hm-accent-ink) 86%, var(--hm-amber)); }
[data-cat-key="Networking"] .meta-cat { color: color-mix(in oklab, var(--hm-accent-ink) 66%, var(--hm-amber)); }
[data-cat-key="Fitness"] .meta-cat { color: color-mix(in oklab, var(--hm-accent-ink) 38%, var(--hm-amber)); }
[data-cat-key="Business Cards"] .meta-cat { color: var(--hm-amber); }
