/* The blog — one essay page, on the design system's marketing layer.
   ================================================================================

   Loaded after `css/tokens.css` and `css/marketing.css`, and resolves against them without
   declaring a token of its own; `e2e/tests/test_marketing_token_isolation.py` pins the values the
   page resolves.

   Chrome — the bar, the bands, the CTAs, the footer and the whole type scale — is the marketing
   layer's. What is here is only what a blog has and a landing page does not: a reading measure, a
   cover, the rendered body of a markdown file, a post row, the line to the latest essay, and the
   subscribe band.

   These classes are `.blog-*` rather than `.rd-mkt-*` on purpose. The marketing layer is the
   design system's, shared by every public page and enforced by prefix
   (`website/tests/test_marketing_layer.py`); this file is one surface's page CSS. A rule that
   turns out to belong to every marketing page moves there and takes the prefix with it.

   ★ Grounds are raw `--sand-*` steps, not `--n-bg-alt`. A marketing page sits on `--sand-1`, one
   step further out than the app, and `--n-bg-alt` is `light-dark(--sand-3, --sand-1)` — which in
   dark IS the page colour. Ink tokens transfer; grounds do not. */

/* ---------------------------------------------------------------- Measure ---- */

/* The reading column, and the page's single measure — the body's own type recipe carries no second
   cap, so there is one number to change.

   660, not the artboard's 720. Measured against the real face at the real size: 720 sets the
   longest paragraph at 80 characters at 1024 and 87 at 1440, where 65-75 is the comfortable band
   for continuous prose. 660 holds it at 73 at every width from 768 up. The artboard drew a column,
   not a measure; this is the number that makes it one. */
.blog-measure {
  max-width: 660px;
  margin: 0 auto;
}

/* ------------------------------------------------------------- Essay head ---- */

/* The essay's title, in the reading voice for the same reason its section headings are. Layered
   over `.rd-mkt-display` rather than replacing it: the tracking and the balanced wrap are the
   layer's and stay.

   ★ The size is fluid, and it is the only fluid value in the system. The display step is a hero's
   size held at three fixed values, but this title is set in `.blog-measure`, whose width is
   `min(720px, viewport - 2 * --mkt-gutter)` — so between 640 and 820 the column shrinks with the
   viewport while the step does not, and a 44px title in a 544-672px column breaks a 70-character
   headline over three lines with both breaks landing on a dangling "your" / "you". A fixed step
   cannot follow a column that moves; this is the case `clamp()` is for, and it is not a third
   breakpoint — the two anchors ARE the system's own two, 30px at 640 and 38px at 1024, with the
   slope between them derived rather than picked (8px over 384px = 2.083vw, intercept 16.67px).
   Below 640 it rests on its floor, which is the value the phone rule set before this replaced it.

   ★ The cap is a measured engine floor, not a taste call, and it moves with the measure. In a
   660px column this headline sets over two lines at 42px in WebKit, 41 in Chromium and 40 in
   Firefox, which shapes Newsreader a hair wider (`text-wrap` is not the cause — balance and pretty
   agree at every size). Sitting on the lowest ceiling IS the boundary, so the cap is 38: two
   pixels of margin under the tightest engine, and all three then set the same two lines. Re-measure
   this if `.blog-measure` changes — it was 42 when the column was 720, and narrowing the column is
   exactly what invalidated it. A longer headline will still wrap to three somewhere and that is
   fine; three in one browser and two in another is not.

   Declared on the article rather than on the title, because the body's section headings are sized
   against it (see `.blog-body h2`): a heading one step under the title has to know what the title
   is, and the title is the one value on this page that moves with the viewport. */
.blog-article {
  --blog-title-size: clamp(30px, 2.083vw + 16.67px, 38px);
}

.blog-title {
  font-family: var(--font-family-serif);
  font-size: var(--blog-title-size);
}

/* Author, date, reading time and the essay's links, in one row. `space-between` puts the links on
   the measure's right edge, which is where the foot used to hold them; below 640 the row wraps and
   they take a line of their own. */
.blog-byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-5);
  font: 400 var(--font-size-small) var(--font-family-sans);
  color: var(--n-text);
}

.blog-byline__who {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

.blog-byline__dates {
  color: var(--n-text-muted);
}

.blog-byline__links {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
}

.blog-byline__links > a {
  position: relative;
  color: var(--n-accent-text);
  text-decoration: none;
}

/* Direct children only: the share panel's links sit inside this row too, and they take the menu
   item's ink, not the byline link's accent.

   The 48 Rule. These paint a ~18px line, and the overlay reaches the floor without painting a
   pixel. Horizontal reach is 7px rather than the half-gap of 8: at 8 the two boxes meet exactly,
   and a shared edge belongs to whichever paints last. Vertically it runs 9px past a 30px row into
   the 24px gap above and the 48px below, both of which hold nothing. */
.blog-byline__links > a::before {
  content: "";
  position: absolute;
  top: 50%;
  right: -7px;
  left: -7px;
  height: 48px;
  transform: translateY(-50%);
}

.blog-byline__links > a:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

@media (hover: hover) {
  .blog-byline__links > a:hover {
    text-decoration: underline;
  }
}

/* ------------------------------------------------------------- Share ----

   One control in the byline. The trigger is a `<summary>` drawn as the design system's secondary
   button at IconButton's 30px height, so it sits level with the avatar; the panel is the Menu
   recipe — raised surface, `--n-border`, `--radius-md`, `--n-shadow-md` — which is the detached
   tier's shape, because a panel that floats over the essay has left the page. Copied from
   `components.css` rather than imported, like the avatar and the field above. */

.blog-share {
  position: relative;
}

.blog-share__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: 30px;
  padding: 0 var(--space-2) 0 var(--space-3);
  border: 1px solid var(--n-border);
  border-radius: var(--radius-sm);
  background: var(--n-surface-raised);
  box-shadow: var(--n-shadow-sm);
  color: var(--n-text);
  font: 500 var(--font-size-small) var(--font-family-sans);
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.blog-share__trigger::-webkit-details-marker {
  display: none;
}

.blog-share__trigger svg {
  color: var(--n-text-muted);
  transition: transform var(--duration-fast) var(--ease-out);
}

.blog-share[open] > .blog-share__trigger svg {
  transform: rotate(180deg);
}

/* The 48 Rule, the byline links' overlay at the trigger's own reach. */
.blog-share__trigger::before {
  content: "";
  position: absolute;
  inset: -9px 0;
}

.blog-share__trigger:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

@media (hover: hover) {
  .blog-share__trigger:hover {
    border-color: var(--sand-8);
  }
}

.blog-share__panel {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: var(--z-menu);
  min-width: 200px;
  padding: var(--space-1);
  background: var(--n-surface-raised);
  border: 1px solid var(--n-border);
  border-radius: var(--radius-md);
  box-shadow: var(--n-shadow-md);
}

/* Links, not roving menu items, so the ring rule is the system's ordinary one: `components.css`
   drops the ring on `.rd-menu__item` only because Radix moves DOM focus with the highlight, and
   nothing here does. */
.blog-share__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  box-sizing: border-box;
  width: 100%;
  min-height: 36px;
  padding: var(--space-2);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--n-text);
  font: 400 var(--font-size-small) / var(--leading-normal) var(--font-family-sans);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.blog-share__item svg {
  flex: none;
  color: var(--n-text-muted);
}

@media (hover: hover) {
  .blog-share__item:hover {
    background: var(--n-accent-tint-strong);
  }
}

.blog-share__item:focus-visible {
  outline: var(--focus-ring);
  outline-offset: -2px;
}

.blog-share__sep {
  height: 1px;
  margin: var(--space-1) var(--space-2);
  background: var(--n-border-subtle);
}

/* Avatar, at the design system's 30px step — `design-system/src/components.css`'s `.rd-avatar`
   plus `.rd-avatar--30`, which is IconButton's box so an avatar lines up with a control beside it.
   Copied rather than imported: `components.css` ships inside the app bundle and no marketing page
   can link it. `website/tests/test_blog_chrome.py` pins the copy against the original. */
.blog-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  overflow: hidden;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-full);
  background: var(--sand-4);
  font-family: var(--font-family-mono);
  font-size: var(--font-size-small);
  font-weight: 500;
  color: var(--n-text-muted);
  user-select: none;
}

/* `.rd-avatar__image`. The portrait fills the same box the initials occupy, so the byline's
   metrics do not move when a post carries one. */
.blog-avatar__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ----------------------------------------------------------------- Cover ---- */

/* Wider than the measure and narrower than the shell: the cover is the one thing on the page
   allowed to break the column, which is what makes it read as the essay's own image rather than a
   figure inside it. Fixed height because the art is chosen to be cropped — `object-fit: cover`
   below — so the band's rhythm does not move with whatever was uploaded. */
.blog-cover {
  max-width: 960px;
  height: 480px;
  margin: var(--space-7) auto 0;
  border: 1px solid var(--n-border-subtle);
  overflow: hidden;
}

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

/* ------------------------------------------------------------ Essay body ----

   The rendered markdown. Selectors are element-level because the HTML comes from
   `website/blog/rendering.py` and carries no classes of its own; every recipe below is a marketing
   type class applied to the tag markdown emits for it. */

.blog-body {
  margin-top: var(--space-7);
}

/* ★ 1260 is `--mkt-compose`, where `--mkt-font-size-read` steps DOWN 21px -> 19px because the
   landing page's composed track sets the same paragraph in 405px. The blog has no track: it keeps
   one 660px column at every width, so the step compensates for something that is not here, and it
   lands the largest screens with the smallest type. Held at the value the tier itself carries from
   640 to 1259, so this changes nothing about the ramp, only where it stops. */
@media (min-width: 1260px) {
  .blog-body {
    --mkt-font-size-read: 21px;
  }
}

/* `.rd-mkt-read` — the page's editorial voice. */
.blog-body p,
.blog-body ul,
.blog-body ol {
  font: 400 var(--mkt-font-size-read) / 1.65 var(--font-family-serif);
  text-wrap: pretty;
  /* A bare URL is one word to the line breaker, and a long one is wider than a 660px column at
     21px. Without this it does not wrap, it overflows — and `overflow-x: clip` on the root means
     the overflow cannot be scrolled to. Markdown authors paste URLs. */
  overflow-wrap: break-word;
}

.blog-body p + p,
.blog-body p + ul,
.blog-body p + ol,
.blog-body ul + p,
.blog-body ol + p {
  margin-top: var(--space-4);
}

.blog-body ul,
.blog-body ol {
  padding-left: var(--space-5);
}

.blog-body li + li {
  margin-top: var(--space-2);
}

/* The essay's headings, in the SERIF family. `guides/type.md`: "Headings are serif in both
   voices." The marketing layer sets its headings in sans, which is right for a landing page and was
   wrong here by inheritance — this surface is prose end to end, and an essay's section breaks belong
   to the reading voice, not to the chrome. The chrome bands on this page ("Earlier essays", the
   subscribe and closing headings) stay sans, which is the Voice Rule working rather than an
   inconsistency: the essay is the conversation, the bands around it are machinery.

   ★ Sized against the prose and the title, NOT taken from the tier by name. `--mkt-font-size-title`
   / `-subtitle` / `-lead` are chrome steps sized for a landing page's bands, and read as steps of
   nothing in particular next to a 21px paragraph: the title step (28/32/30) put the section heading
   ABOVE the essay's own title from 640 to 1024, where the title rests at 30-33px — and at 1.27
   under it at 1440, where "Are you solving the right problem?" read nearly as loud as the headline.
   The subtitle step (21/22/20) sat one pixel UNDER the paragraph at 1260+, and the lead step
   (17/19/17) under it at every width. A heading that is smaller than the prose it heads is not a
   heading.

   The h2 sits at the geometric midpoint between the title and the prose — the one size that is the
   same ratio under the title as it is over the paragraph. CSS has no square root, so two linear
   terms stand in for it: four thirds of the reading size, capped at four fifths of the title. The
   pair lands within a pixel of sqrt(title x read) at every width that matters — 24 / 24 / 26.7 / 28
   at 390 / 640 / 800 / 1024+, against 23.2 / 25.1 / 26.4 / 28.2 exact — and it is the cap that does
   the work from 640 to ~900, where the title rests on its 30px floor over a 21px paragraph and a
   1.43 span has to hold three sizes. That band is compressed by construction and the midpoint is
   the fairest split of it. At 1440 the title is 1.36 over the h2 and the h2 1.33 over the prose.

   Below 640 both terms give 24, which is the off-scale literal the phone rule used to set by hand
   (1.33 over an 18px body, and "Are you solving the right problem?" on one line); it is no longer
   a literal, it is what the formula lands on.

   Tracking is 0, not the sans title recipe's -0.015em: Newsreader carries an optical-size axis and
   tightens itself at these sizes; measured against -0.005em at 28px the two are a tenth of a pixel
   apart, and the sans figure was inherited, not chosen. */
.blog-body {
  --blog-h2-size: min(
    calc(var(--mkt-font-size-read) * 4 / 3),
    calc(var(--blog-title-size) * 0.8)
  );
}

.blog-body h2 {
  margin-top: var(--space-7);
  font: 500 var(--blog-h2-size) / 1.2 var(--font-family-serif);
  letter-spacing: 0;
  text-wrap: pretty;
}

/* Halfway between the h2 and the prose — 21 / 22.5 / 23.8 / 24.5 — for the same reason the h2 is
   halfway between the title and the prose: the level below always has somewhere to go. A fixed
   ratio over the reading size (1.2 gives 25.2) overtakes the h2 wherever the cap has it at 24. */
.blog-body h3 {
  margin-top: var(--space-6);
  font: 500 calc((var(--blog-h2-size) + var(--mkt-font-size-read)) / 2) /
    var(--leading-tight) var(--font-family-serif);
}

/* Markdown has six levels and there is no room for three more sizes under the h3, so h4 sits at
   the reading size and is a heading by weight, and h5/h6 share that box in italic — the same
   collapse `.rd-prose` documents rather than inventing a step. Never under the prose: the lead
   step this used to take is 17px against a 21px paragraph. */
.blog-body h4,
.blog-body h5,
.blog-body h6 {
  margin-top: var(--space-5);
  font: 500 var(--mkt-font-size-read) / var(--leading-tight) var(--font-family-serif);
}

.blog-body h5,
.blog-body h6 {
  font-style: italic;
}

.blog-body h2 + *,
.blog-body h3 + *,
.blog-body h4 + *,
.blog-body h5 + *,
.blog-body h6 + * {
  margin-top: var(--space-4);
}

/* Underlined, not accent-coloured alone. A link inside a sentence that differs from the prose
   only in hue fails WCAG 1.4.1 unless the two inks clear 3:1 between themselves, and
   `--n-accent-text` against `--n-text` does not. This is `.rd-prose a`'s recipe exactly, hover
   included: thickness rather than a colour change, because there is no accent-text hover step. */
.blog-body a {
  color: var(--n-accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

@media (hover: hover) {
  .blog-body a:hover {
    text-decoration-thickness: 2px;
  }
}

.blog-body a:active {
  text-decoration-thickness: 2px;
}

.blog-body img {
  display: block;
  max-width: 100%;
  height: auto;
  margin: var(--space-6) 0;
}

.blog-body blockquote {
  margin-top: var(--space-5);
  padding-left: var(--space-4);
  border-left: 2px solid var(--n-accent);
  color: var(--n-text-muted);
}

.blog-body hr {
  height: 1px;
  margin: var(--space-6) 0;
  border: none;
  background: var(--n-border-subtle);
}

.blog-body code {
  font-family: var(--font-family-mono);
  font-size: 0.9em;
  color: var(--n-code-text);
  background: var(--sand-3);
  padding: 0.1em 0.3em;
  border-radius: var(--radius-sm);
}

/* `.highlight` is Pygments' own wrapper (`css/blog-code-highlight.css`), which brings a hardcoded
   theme background with it. The listing's ground is the page's decision, not the highlighter's. */
.blog-body pre,
.blog-body .highlight {
  margin-top: var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--n-border-subtle);
  background: light-dark(var(--sand-2), var(--sand-3));
  overflow-x: auto;
}

/* Markdown emits <pre><code>, so without this the inline chip above applies a second time inside
   the block — the whole listing in the code ink, inset by the chip's padding and rounded. */
.blog-body pre code,
.blog-body .highlight pre,
.blog-body .highlight code {
  font-size: var(--font-size-small);
  line-height: var(--leading-loose);
  color: inherit;
  background: none;
  padding: 0;
  border: none;
  border-radius: var(--radius-none);
}

/* A listing wider than the measure scrolls, and `rendering.py` makes it focusable so a keyboard
   can reach the scroll; a focusable box with no ring is the failure WCAG 2.4.7 names. */
.blog-body pre:focus-visible,
.blog-body .blog-table:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

.blog-body .mermaid {
  margin-top: var(--space-6);
}

/* A markdown table, in the wrapper `rendering.py` puts round it so that a table wider than 660px
   scrolls instead of being clipped by the root's `overflow-x: clip`. On the design system's
   hairline language — `.rd-prose table`'s recipe, brought up to this page's sizes: a rule under
   every row, no verticals, no ground. Sans at the body step, not the reading size: a table is
   scanned, not read, and it sits in the machinery voice for the same reason the byline does. The
   design system's own table runs at `--font-size-small`, which is 13px beside a 21px paragraph
   here — a drop that reads as fine print rather than as a table. */
.blog-body .blog-table {
  overflow-x: auto;
  margin-top: var(--space-5);
}

/* A table ends on a rule and a listing on a border, and the paragraph rhythm above only knows
   `p + p`; without this the next paragraph sat on the line. Prose only: a `+ *` here outranks
   `.blog-body h2` and would pull a heading that follows a listing down to the paragraph gap. */
.blog-body .blog-table + p,
.blog-body .blog-table + ul,
.blog-body .blog-table + ol,
.blog-body pre + p,
.blog-body pre + ul,
.blog-body pre + ol,
.blog-body .highlight + p,
.blog-body .highlight + ul,
.blog-body .highlight + ol {
  margin-top: var(--space-4);
}

.blog-body table {
  /* Natural column widths inside the scroll box. At `width: 100%` an overflowing table keeps
     trying to fit: the last column is squeezed to a sliver off-canvas and every visible row
     inflates to hold its wrapped cells, so the phone reader sees blank row height and no cue
     that a column exists. `min-width` keeps a narrow table on the measure's edges. */
  width: max-content;
  min-width: 100%;
  border-collapse: collapse;
  font: 400 var(--font-size-body) / var(--leading-normal) var(--font-family-sans);
}

.blog-body th,
.blog-body td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--n-border-subtle);
  text-align: left;
  vertical-align: top;
}

/* The header row ends on the full-strength hairline, the body rows on the subtle one — the same
   two weights that separate a head from its rows everywhere else in the system. */
.blog-body th {
  font-weight: 500;
  border-bottom-color: var(--n-border);
}

/* The first cell of a row sits on the measure's left edge, like the prose above it, and the last
   on its right; the padding is between cells, not around the table. */
.blog-body th:first-child,
.blog-body td:first-child {
  padding-left: 0;
}

.blog-body th:last-child,
.blog-body td:last-child {
  padding-right: 0;
}

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

   One line under an older essay pointing at the newest. "Earlier essays" only points backwards,
   so a reader who arrived at an old essay from a shared link had no route forward except the nav
   word "Blog". Absent from `/blog/`, which IS the newest essay.

   Not a band of its own: `.rd-mkt-band` would put a section's worth of air above one sentence and
   another above "Earlier essays" beneath it. It hangs off the essay's foot instead, one paragraph
   gap down, and reads as the essay's own tail. */
.blog-latest {
  padding: var(--space-7) var(--mkt-gutter) 0;
  font: 400 var(--font-size-body) var(--font-family-sans);
  color: var(--n-text-muted);
}

.blog-latest a {
  position: relative;
  color: var(--n-accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The 48 Rule, as the byline's links have it. */
.blog-latest a::before {
  content: "";
  position: absolute;
  top: 50%;
  right: -8px;
  left: -8px;
  height: 48px;
  transform: translateY(-50%);
}

.blog-latest a:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

@media (hover: hover) {
  .blog-latest a:hover {
    text-decoration-thickness: 2px;
  }
}

/* ------------------------------------------------------------ A post row ----

   A row under "Earlier essays", which is the blog's whole archive — there is no list page. The
   root lists all of them; a post page lists the nearest few. */

.blog-entry {
  padding: var(--space-6) 0;
  border-bottom: 1px solid var(--n-border-subtle);
}

/* Serif, one step over the dek it heads. The row used to take `.rd-mkt-title` — 30px DM Sans —
   which is a section header's recipe in the chrome voice, on an all-serif page whose own section
   headings are 28px serif; the archive row shouted over the essay above it in the wrong family.
   A row is not a section: its title heads one paragraph, the dek, so it takes the 1.2 step over
   the dek's size and moves with it. The dek is `.rd-mkt-read` at the tier's own reading size (18 /
   21 / 19), not the essay's restored one — so this is 21.6 / 25.2 / 22.8 and stays 1.2 over the
   dek at every width, where the subtitle step by name would be one pixel over it at 1260+. */
.blog-entry__title {
  margin: 0;
  font: 500 calc(var(--mkt-font-size-read) * 1.2) / var(--leading-tight)
    var(--font-family-serif);
  text-wrap: pretty;
}

/* The title is the row's only link. A "Continue reading" line under the dek used to point at the
   same URL, which was two links to one place with the biggest word on the row already the target.
   It carries the page's ink rather than the accent, so the row still reads as a heading over a
   link. */
.blog-entry__title a {
  color: inherit;
  text-decoration: none;
}

@media (hover: hover) {
  .blog-entry__title a:hover {
    text-decoration: underline;
    text-underline-offset: 4px;
  }
}

.blog-entry__title a:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

.blog-entry__dates {
  margin-top: var(--space-2);
  font: 400 var(--font-size-small) var(--font-family-sans);
  color: var(--n-text-muted);
}

.blog-entry__dek {
  margin-top: var(--space-3);
  color: var(--n-text-muted);
}

/* --------------------------------------------------------------- Closing ---- */

/* The landing page's closing band ends a page that has been drawing its own sections all the way
   down. The blog arrives at it off the subscribe band, which is quieter than a section and draws
   nothing, so without a rule here the last two things on the page are separated by air alone. */
.blog-close {
  border-top: 1px solid var(--n-border-subtle);
}

/* `.rd-mkt-read` sets a 70ch box with `margin: 0`, which is a column of prose left-aligned in its
   parent. Under a centred heading the box has to be centred as well, and it has to be narrower
   than the measure prose wants: one centred sentence at 70ch sets a ragged block wider than the
   title above it, which reads as a stray paragraph rather than as the heading's second line. */
.blog-close__lead {
  max-width: 52ch;
  margin: var(--space-4) auto 0;
}

/* ------------------------------------------------------------- Subscribe ---- */

/* A quieter band than `.rd-mkt-band`, and the one place on the page that owns air on both edges:
   it sits between two rules — the post list's last row above, the closing band's border below —
   and a band that only owned its top would press its second heading against the line under it. */
.blog-subscribe {
  padding: var(--space-7) var(--mkt-gutter);
}

/* `minmax(0, 1fr) auto`, not `1fr 1fr`. Two equal tracks were never equal: a `1fr` track's floor is
   its content's min-content, and the form does not shrink below its field plus its button, so the
   heading got whatever was left — 264px at 1440 and "…when it / lands." on a line of its own.
   Saying it outright puts the form at its own width and the heading in the rest, and `balance` on
   the heading is what stops the widow.

   Two columns only from `--breakpoint-desktop`. The form is 356px whatever the viewport, so in a
   544px column at 640 the heading had 140px and set over three lines beside it. There is no width
   between 640 and 1024 at which the pair fits and no third breakpoint to put one at, so the band
   stacks below the desktop boundary and the heading takes its one line over the form. */
.blog-sub {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-7);
  align-items: center;
}

.blog-sub h2 {
  text-wrap: balance;
}

/* 1023 is one below `--breakpoint-desktop`. `minmax(0, 1fr)`, not `1fr`: a `1fr` track's automatic
   minimum is `min-content`, so the form inside it can push the track wider than the viewport —
   which it did at 200% browser font, where the row reached 426px on a 390px screen. `overflow-x:
   clip` on html/body then means there is no scrollbar and no way to reach what was cut off. */
@media (max-width: 1023px) {
  .blog-sub {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
  }
}

/* Two columns exist to seat a form beside its heading. With no provider configured there is no
   form, and the second column holds four words of feed link beside 336px of nothing, which reads
   as unfinished rather than as quiet. Stated as "I contain no form" rather than as a flag the page
   passes in, for the same reason the rule below is adjacency: that is the actual condition. */
.blog-sub:not(:has(form)) {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
}

/* A top rule only when nothing above it already ended on one — which is exactly "the essay, or
   the latest-essay line hanging off it, is the previous sibling". With an "Earlier essays" section
   between them, that section's last row has drawn the line already. Stated as adjacency rather
   than as a flag the page passes in, because adjacency is the actual condition. */
article + .blog-subscribe .blog-sub,
.blog-latest + .blog-subscribe .blog-sub {
  padding-top: var(--space-6);
  border-top: 1px solid var(--n-border-subtle);
}

.blog-sub-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.blog-sub-form > .blog-field {
  flex: 1;
  min-width: 0;
}

/* Field, from `design-system/src/components.css`'s `.rd-field__control` — same ground, border,
   shadow and transition — squared off, because the marketing CTA it sits against is square by
   intent and a 2px radius beside a 0px one at 48px reads as a mistake rather than a choice.
   `website/tests/test_blog_chrome.py` pins the copy against the original. */
.blog-field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--n-bg);
  border: 1px solid var(--n-border);
  border-radius: var(--radius-none);
  box-shadow: var(--n-shadow-sm);
  transition:
    border-color var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out);
  /* Matched to the CTA beside it so the row reads as one control rather than two. */
  min-height: var(--mkt-cta-height);
}

/* A recolour, not an outline. `components.css` states this one in capitals: an outline inset beside
   a resting 1px border draws two parallel strokes for a single state. The ring belongs to the
   container rather than the input, which is the whole reason Field is a component. */
.blog-field:focus-within {
  border-color: var(--n-focus-ring);
}

.blog-field input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  color: var(--n-text);
  font: 400 var(--font-size-body) var(--font-family-sans);
}

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

.blog-field input::placeholder {
  color: var(--n-text-muted);
}

.blog-sub-note {
  margin-top: var(--space-3);
  font: 400 var(--font-size-small) var(--font-family-sans);
  color: var(--n-text-muted);
}

/* A link inside a sentence, so it takes `.blog-body a`'s recipe: hue alone does not clear 3:1
   against the muted note ink around it (1.10:1 light, 1.01:1 dark), which is what WCAG 1.4.1
   needs before an underline can be dropped. `white-space: nowrap` because "RSS feed" wrapping at
   390 draws the focus ring as two boxes. */
.blog-sub-note a {
  position: relative;
  color: var(--n-accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
  white-space: nowrap;
}

.blog-sub-note a::before {
  content: "";
  position: absolute;
  top: 50%;
  right: -8px;
  left: -8px;
  height: 48px;
  transform: translateY(-50%);
}

.blog-sub-note a:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

@media (hover: hover) {
  .blog-sub-note a:hover {
    text-decoration-thickness: 2px;
  }
}

/* ------------------------------------------------------------ Responsive ----

   639 is one below `--breakpoint-mobile`, the design system's own. Type, gutters, band rhythm, the
   nav's fold and the footer's stacking are all the marketing layer's at the same boundary; what is
   here is only what the blog adds. */
@media (max-width: 639px) {
  /* An essay is not a pitch. The bar's filled CTA rides over 7,000px of reading on a phone and is
     the loudest thing on the page for the whole of it, and at 350px of usable width it crowds the
     wordmark and the menu button besides. The offer moves into the panel, one tap from every scroll
     position, which is what `.rd-mkt-nav__panel-cta` exists for — the closing band and the footer
     still carry it in flow. The bar keeps the wordmark and the menu, and goes quiet. */
  .rd-mkt-nav__actions .rd-mkt-cta--primary {
    display: none;
  }

  .rd-mkt-nav__panel-cta {
    display: block;
  }

  /* The title's own size is the clamp above, resting on its 30px floor here; what is left is the
     leading and tracking, which `.rd-mkt-display` sets for a sans hero and which a serif at this
     size wants a little looser. The section heading needs nothing here: its formula lands on 24
     below 640 on its own (see `.blog-body h2`). */
  .blog-title {
    line-height: 1.22;
    letter-spacing: -0.01em;
  }

  /* The cover is cropped art, so a phone gets less of it rather than a shorter page with the same
     framing squashed into it. */
  .blog-cover {
    height: 220px;
  }

  /* `.rd-menu--touch`: below the breakpoint a panel row is the most-tapped target there is. The
     panel hangs from the trigger's left edge because the links wrap under the author here, and a
     right-anchored panel would open off the measure's left. */
  .blog-share__item {
    min-height: var(--space-7);
  }

  .blog-share__panel {
    right: auto;
    left: 0;
  }

  /* Two 48px controls side by side leave the field about eleven characters wide at 390. */
  .blog-sub-form {
    flex-direction: column;
    align-items: stretch;
  }
}
