/**
 * Theme tokens.
 *
 * The palette follows aihot.news: a warm paper ground rather than pure white, a
 * deep teal accent instead of a bright blue, and four ink steps instead of one.
 * The dark theme is a separately chosen set of values, not an inversion — an
 * inverted palette lands on muddy greys, and this one is picked so the teal reads
 * as brighter on the dark ground (#12ccd8) while the paper one stays a deep ink
 * (#176b75) on the light ground. Same hue family, different lightness, because the
 * contrast ratio has to work in both directions.
 *
 * `data-theme` on <html> is set by an inline script before first paint (see
 * Base.astro), so there is no flash. The media query is a fallback for the no-JS
 * case: with scripting off, `data-theme` is never written and the system preference
 * decides.
 */

:root {
  color-scheme: light;

  --bg: #faf9f6;
  --panel: #fff;
  --panel-2: #f3f3ef;
  --border: #dfe4e1;
  --border-soft: #e9ede9;
  --text: #202a30;
  --muted: #59656b;
  --faint: #657176;
  --accent: #176b75;
  --accent-dim: #145f68;
  --accent-contrast: #fff;
  --ok: #2f7d5c;
  --warn: #b8873a;
  --danger: #b3402a;

  /*
   * Elevation, per theme.
   *
   * A token rather than a literal because the same shadow does not read the same on both
   * grounds: black at 12% over a near-white page is a visible lift, and over a near-black
   * one it is nearly invisible. The dark value is therefore both darker and larger.
   *
   * This is the only shadow in the stylesheet — the account menu is the only thing that
   * floats above the page.
   */
  --shadow-menu: 0 8px 24px rgb(0 0 0 / 0.12);

  /* Non-token values, shared by both themes. */
  --radius: 10px;
  --wrap: 1180px;

  /*
   * The x.ai plugin banner is a dark image in both themes — it is the page's own
   * Open Graph art — so its label keeps its own ground instead of inheriting the
   * paper's, which would put white-on-white in light mode and black-on-black in
   * dark mode.
   */
  --app-chip-bg: rgba(0, 0, 0, 0.72);
  --app-chip-ink: #ffffff;
  /* X's own seal blue, used only for the verified mark on a quoted post. */
  --verified: #1d9bf0;

  /*
   * Type.
   *
   * The archive is mostly Chinese, so the CJK faces are not an afterthought — they
   * are the primary rendering, and Latin is the fallback within each family. A
   * stack that stops at `system-ui` hands every Chinese glyph to whatever the OS
   * picks with no control over it, and the four platforms disagree.
   *
   * `Inter` leads the sans stack because it has no CJK coverage, so it only ever
   * paints Latin: a face that cannot render a glyph is skipped and the next family
   * takes over per-character. That is also why a locally-installed Inter changes
   * the Latin text and leaves the Chinese untouched, which is the correct
   * behaviour. No webfont is loaded — the first family is a local-name lookup, so
   * the stack costs nothing when the font is absent and falls through to the OS.
   *
   * The reading face is a sans, not a serif. `ui-serif` resolves Chinese to Songti
   * SC, whose thin strokes and tight default rhythm are the worst case at 17.5px on
   * screen; a PingFang / Noto Sans face holds its weight and tracks the 1.75
   * leading properly. Latin keeps a serif for the headline treatment, where the
   * contrast is a design choice rather than a legibility one.
   */
  --sans:
    "Helvetica Neue", Helvetica, Inter, "PingFang SC", "PingFang TC", "Microsoft YaHei", "Microsoft JhengHei",
    "Noto Sans CJK SC", "Noto Sans CJK TC", "Hiragino Sans GB", -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, system-ui,
    sans-serif;
  --serif:
    Georgia, "Iowan Old Style", "Times New Roman", "Songti SC", STSong,
    "SimSun", "Noto Serif CJK SC", "Source Han Serif SC", serif;
  --mono:
    ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono",
    "Noto Sans Mono CJK SC", monospace;

  /* The header floats over scrolling content, so it needs a translucent ground.
     Declared per theme because a fixed alpha over a dark paper and a light paper
     do not produce the same result. */
  --header-bg: #faf9f6e8;

  /*
   * The logo is not a token any more.
   *
   * It used to be a dark tile with a light letter in it, deliberately unthemed so
   * that it kept matching a favicon which could not adapt — inverting the in-page
   * mark while the tab stayed fixed would have made one brand read two ways. That
   * premise is gone: the favicon now ships its own inverse and the tab adapts with
   * the page, so the two agree in both themes and there is nothing left to hold
   * still. The mark is also a near-black silhouette rather than a coloured tile, so
   * on the dark ground it would not be a mismatch but an absence — pinned, it
   * measures a contrast ratio near 1.1 against #13191c. See `.logo-mark` below.
   */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    color-scheme: dark;
    --bg: #13191c;
    --panel: #1a2226;
    --panel-2: #202a2f;
    --border: #ffffff26;
    --border-soft: #ffffff17;
    --text: #e7eceb;
    --muted: #a5b0b3;
    --faint: #89979d;
    --accent: #12ccd8;
    --accent-dim: #2ce2e8;
    --accent-contrast: #000;
    --ok: #5fc79a;
    --warn: #d3b26a;
    --danger: #d86a52;
    --header-bg: #13191ce8;
  }
}

/* The explicit dark choice, and the explicit light choice. Both are needed: the
   media query above only covers "the user has expressed no preference", so a
   reader who picked light on a dark-mode machine needs this to win. */
:root[data-theme='dark'] {
  color-scheme: dark;
  --bg: #13191c;
  --panel: #1a2226;
  --panel-2: #202a2f;
  --border: #ffffff26;
  --border-soft: #ffffff17;
  --text: #e7eceb;
  --muted: #a5b0b3;
  --faint: #89979d;
  --accent: #12ccd8;
  --accent-dim: #2ce2e8;
  --accent-contrast: #000;
  --shadow-menu: 0 10px 30px rgb(0 0 0 / 0.5);
  --ok: #5fc79a;
  --warn: #d3b26a;
  --danger: #d86a52;
  --header-bg: #13191ce8;
  /* The x.ai banner is a dark image in both themes; its label keeps its own ground. */
  --app-chip-bg: rgba(0, 0, 0, 0.72);
  --app-chip-ink: #ffffff;
  --verified: #1d9bf0;
}

:root[data-theme='light'] {
  color-scheme: light;
  --bg: #faf9f6;
  --panel: #fff;
  --panel-2: #f3f3ef;
  --border: #dfe4e1;
  --border-soft: #e9ede9;
  --text: #202a30;
  --muted: #59656b;
  --faint: #657176;
  --accent: #176b75;
  --accent-dim: #145f68;
  --accent-contrast: #fff;
  --shadow-menu: 0 8px 24px rgb(0 0 0 / 0.12);
  --ok: #2f7d5c;
  --warn: #b8873a;
  --danger: #b3402a;
  --header-bg: #faf9f6e8;
  --app-chip-bg: rgba(0, 0, 0, 0.72);
  --app-chip-ink: #ffffff;
  --verified: #1d9bf0;
}

* { box-sizing: border-box; }

.skip-link {
  clip: rect(0 0 0 0);
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  white-space: nowrap;
  font-size: 13px;
  line-height: 1.5;
}
.skip-link:focus {
  clip: auto;
  width: auto;
  height: auto;
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 1000;
  padding: 8px 14px;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 7px;
  color: var(--accent);
  font-size: 13px;
  line-height: 1.5;
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--shadow-menu);
}

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* 16px, not 15px. Chinese glyphs occupy the full em box, so 15px CJK is below
     the comfortable floor, especially in a mixed Latin/numeric article. */
  font: 400 16px/1.75 var(--sans);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-kerning: normal;
}

/*
 * Body prose gets 1.75; everything else gets 1.5.
 *
 * Chinese needs the extra leading in body text — adjacent full-width lines
 * otherwise come within a couple of pixels of touching. Nothing else does:
 * letting 1.75 cascade to 11–13px labels put them in 21px line boxes and opened
 * visible gaps between a card title and its author, because 12px text does not
 * need 75% leading.
 *
 * Deliberately only the elements that had no line-height of their own. Anything
 * with a specific value keeps it, which means this block must not name it — a
 * rule here and the specific one elsewhere would tie on specificity and the
 * winner would come down to source order, which is not a property worth relying
 * on. Unitless throughout, so a nested element still scales its line box from
 * its own size.
 */
/*
 * Everything that is not body prose gets 1.5.
 *
 * Scoped to `.wrap` and `:root` descendants so the reading surface is not caught
 * by the element selectors below. An earlier version listed bare `p` and `ul`,
 * which matched `.article-body p`: they tied on specificity and won by source
 * order, silently collapsing body text from 30.6px back to 26.25px — the exact
 * leading this was raised to fix. Excluding them with `:not(.article-body p)` also
 * works, but that puts a compound selector inside `:not()`, which is Selectors 4
 * and unsupported before Safari 16.
 *
 * The descendant selectors are only there to raise specificity above the bare
 * element rules; `.article-body` is not among them, so a paragraph inside it can
 * only match `.article-body p` and keeps the container's inherited 1.75.
 */
.hero-sub, .board-title, .board-more, .pager, .pager a,
.card-meta, .card-foot, .badge, .cat-chip, .cat-count,
.tag, .nav-item, .search-mini input, .category-select,
.search-form input, .search-form button, .result-snippet, .article-crumbs,
.original-link, .byline, .hero-meta, .footer-inner, .fig-missing, .empty-state,
pre, .card-article, h1, h2, h3, h4, h5, h6, ul, ol, blockquote,
.hero h1, .stat-row, .author-name .handle, .fig-missing span,
.auth-link, .auth-menu-item, .save-button, .banner, .account-email {
  line-height: 1.5;
}

/* Restated after the block above so the reading surface wins on source order
   rather than on a `:not()` compound, which older Safari cannot parse.
   `pre` is excluded: a code block wants 1.5, not the prose leading, and it
   already declares that. */
.article-body,
.article-body p,
.article-body h1,
.article-body h2,
.article-body h3,
.article-body ul,
.article-body ol,
.article-body blockquote {
  line-height: 1.75;
}

a { color: inherit; text-decoration: none; }
a:hover { color: var(--accent); }

/*
 * One focus ring for the whole site. Everything focusable gets it, and nothing may
 * take it away.
 *
 * It was three standards, not one. Inputs and selects suppressed the outline and
 * shifted the border colour to `--accent-dim`, which is close to invisible for
 * anyone who cannot separate that hue from the hairline it borders. The theme and
 * language switches drew a 2px accent ring at +1px. The admin drew the same ring at
 * -1px. Two of the three were right; the one that was wrong was the one on the
 * elements a reader actually types into.
 *
 * `-1px` — inset — rather than outset, for a reason the layout forces: `.card-article`
 * is `overflow: hidden` because that is what clips its cover image to the card's
 * radius, so an outset ring around the card's title link is cut off on both sides and
 * the reader gets a pair of brackets instead of a ring. Inset is drawn inside the
 * border box and is never clipped by any ancestor. It also lets a borderless link
 * show one, where an outset ring would need padding the link does not have.
 *
 * `:focus-visible`, not `:focus`, so a mouse click on a link does not paint a ring —
 * while a text input still gets one on click, because the caret is there and the
 * reader needs to see it.
 *
 * No `border-radius` here on purpose. An outline already follows its element's own
 * radius, so a 7px input draws a 7px ring and a square link draws a square one.
 * Declaring a radius would give every element the same ring shape and break the
 * ones that are actually round.
 *
 * Zero specificity via `:where()` on purpose. A component may still *add* to this —
 * an input darkens its border as well, which is a second cue, not a substitute —
 * but no component rule can quietly match it away again.
 */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 24px; }

/* --- header --- */

.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--header-bg);
  backdrop-filter: blur(12px);
  position: sticky;
  top: 0;
  z-index: 20;
}

.header-inner {
  display: flex;
  align-items: center;
  /* 18px, down from 24px. The site name is no longer drawn at any width, so this row's
   * spacing is set once rather than changed at a breakpoint — and every pixel here is
   * taken from the nav, which is the one thing that must never scroll. */
  gap: 18px;
  height: 58px;
}

.logo { display: flex; align-items: center; gap: 9px; font-weight: 700; letter-spacing: -0.02em; }
/*
 * The brand mark, as the artwork from Logo.jpg rather than a tile and a letter.
 *
 * Two rasters ship and one is shown: the mark is near-black by construction, so on
 * the dark ground it is not low contrast, it is absent. A CSS filter is not the
 * answer either — inverting the gold star through the complement lands on teal,
 * which on this palette is the accent colour, so the star would read as interface
 * rather than as artwork. Two files also means the dark variant is the same
 * artwork, not a filter's idea of it.
 *
 * The intrinsic size is 809x887, so at 24px tall the mark is 22px wide. Sizing by
 * height and letting the width follow the element's own aspect ratio is what keeps
 * it undistorted; the `width`/`height` attributes on the element hold the same
 * ratio so the flex row reserves the right box before the image loads.
 */
.logo-mark {
  display: block;
  height: 24px;
  width: auto;
}
.logo-mark-inverse { display: none; }
:root[data-theme='dark'] .logo-mark { display: none; }
:root[data-theme='dark'] .logo-mark-inverse { display: block; }

/*
 * The no-JS case. `data-theme` is written by the inline script in Base.astro, so
 * without scripting it is never set and the system preference is the only signal
 * there is — the same fallback the token blocks above make, and the same shape:
 * `:not([data-theme='light'])` rather than a bare media query, so a reader who
 * chose light on a dark-mode machine still gets light.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .logo-mark { display: none; }
  :root:not([data-theme='light']) .logo-mark-inverse { display: block; }
}
/*
 * The site name is announced but no longer drawn.
 *
 * It used to be visible above 1300px and hidden below it. Moving the category filter into
 * the header row is what ended that: the row gained a ~152px `<select>`, and the column is
 * `--wrap` wide — 1180px of content at *every* viewport from 1301px up, so a wider monitor
 * buys nothing. Measured in headless Chrome against the deployed header, with the Chinese
 * labels (the widest of the three): the nav needs 372px and the row gave it 288px. Every
 * combination that kept the wordmark — shrinking the search box, the select, or the header
 * gap — landed within ±10px of that line, which is no margin at all.
 *
 * So the name gives way, per the ladder this row has always followed: primary navigation
 * never gives way, and the site name is the first thing to go because the mark beside it
 * and the document title already carry it.
 *
 * `visually-hidden` and not `display: none`, because both marks are `alt=""` — deliberate,
 * so a screen reader does not hear the name twice — which makes this text the logo link's
 * only accessible name. Removing it would announce the link as empty on every page.
 */
.logo-text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.nav { display: flex; gap: 2px; flex: 1; overflow-x: auto; }
.nav-item {
  display: inline-flex; align-items: center; gap: 6px;
  /* 9px, not 10px. Measured with the sixth item: six labels at 14px/10px need 558px and
   * the row has 553px at every width from 1301px up, so the last item was short by 5px on
   * a desktop — the same non-obvious overflow 11px caused the first time, and the same
   * kind of reading (a rendering quirk rather than a layout fault). 1px a side is 12px
   * back, which is what it takes. The 700px block below takes it to 9px for narrow rows,
   * and this is now the same number, so the two no longer differ in padding at all. */
  padding: 6px 9px;
  border-radius: 7px;
  color: var(--muted);
  font-size: 14px;
  white-space: nowrap;
}
/*
 * Three levels, not two: idle, hover, current.
 *
 * `current` is the theme switch's own selected state — an `--accent` fill with
 * `--accent-contrast` lettering — so the accent now means exactly one thing across the
 * whole site: you are on this. It was worth copying rather than inventing, because the rule
 * it replaces was not subtle-but-arguable, it was invisible: `--panel-2` on the header is
 * a contrast ratio of 1.06 in the light theme and 1.21 in the dark one, so a reader could
 * not tell which board they were on. The accent fill measures 5.9:1 against the header in
 * light and 9.0:1 in dark, with 6.2:1 and 10.7:1 between its own fill and its lettering.
 *
 * The category page's sort chips answer the same question with a `--text` fill and do carry a
 * `font-weight` change. Neither is copied here, and the reason is not that they are wrong. The
 * text fill is louder, and louder is not the goal. The weight is left out for a measured reason:
 * it widens the label, and the nav has no slack, so the row would change width on every
 * navigation.
 *
 * Those chips live in the page body, not the header, so the two treatments cannot meet. Worth
 * knowing before moving them: a `--text` pill and an `--accent` pill side by side in one header
 * would read as two systems.
 *
 * Hover stays neutral on purpose. A tinted hover would compete with the accent for the
 * meaning "selected", so hover is carried by the lettering instead — `--muted` is 5.7:1
 * against the header and `--text` is 13.9:1 — with `--panel-2` riding along as a faint
 * raised surface. That fill is nearly invisible on its own, which is the same 1.06 as the
 * old active state; the difference is that here it is decoration behind a cue that works.
 */
.nav-item:hover { background: var(--panel-2); color: var(--text); }
.nav-item.active,
.nav-item.active:hover { background: var(--accent); color: var(--accent-contrast); }
/*
 * The nav glyph is a filled Phosphor path inlined by `Icon.astro`, at the size the call
 * site asks for rather than one fixed here.
 *
 * `flex: none` is the load-bearing part. The nav row is `overflow-x: auto` so it can
 * scroll on a narrow screen, and a flex item's default is to shrink — without this the
 * glyphs would be the first thing to squash, and every one of them would deform before
 * any label did. Dimming slightly puts the icon behind its label instead of beside it at
 * equal weight.
 */
.nav-icon {
  flex: none;
  opacity: 0.85;
}

/* SPEC §15 — the category filter, inside the search form. */
.search-mini {
  display: flex;
  align-items: center;
  /* 6px, not 8px. Every pixel of this pair's width is taken straight out of the nav, which
   * is the one thing in the row that must never scroll (see `.nav-item` below). */
  gap: 6px;
}
.search-mini input {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 7px;
  color: var(--text);
  padding: 6px 11px;
  font-size: 13px;
  /* 170px, down from 190px. Measured in English, which is the widest of the three labels:
   * the nav needs 462px and the row has 1132px of content, so the pair can afford at most
   * ~250px with any margin left over. */
  width: 170px;
}
.search-mini input:focus { border-color: var(--accent-dim); }

/*
  The select is styled to the same measure as the search box next to it — same border, radius,
  padding and text size — so the pair reads as one control rather than as a filter with a search
  field attached.

  108px is a measured ceiling, not a taste: the longest option that has to fit un-truncated is
  "Technology" at 66px, plus 11px left padding and 26px of caret gutter. Fixed rather than
  auto-sizing because a variable width makes the search box jump as the selection changes.

  `appearance: none` is what keeps the control identical across platforms; the native arrow
  renders at a different size and colour on macOS, Windows and Android, and the padding it
  reserves leaves the label off-centre.

  The caret is a masked sibling in `.category-filter`, not a background image here and not a
  `::after` — a `<select>` has no generated children that render. A data-URI SVG had to name its
  stroke colour, so it was hardcoded `#787f8a`: it could not follow the theme, and it could not
  invert when the control marks the category the reader is already on. A mask inherits its colour
  from `currentColor`, so one declaration now serves both themes and every state.
*/
.category-filter {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.category-select {
  appearance: none;
  -webkit-appearance: none;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 7px;
  color: var(--text);
  cursor: pointer;
  font-size: 13px;
  padding: 6px 26px 6px 11px;
  width: 108px;
  text-overflow: ellipsis;
  transition: color 0.12s ease, background-color 0.12s ease, border-color 0.12s ease;
}
.category-filter__chevron {
  position: absolute;
  right: 9px;
  top: 50%;
  margin-top: -6px;
  width: 12px;
  height: 12px;
  color: var(--faint);
  background: currentColor;
  pointer-events: none;
  transition: color 0.12s ease;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E")
    no-repeat center / 12px;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E")
    no-repeat center / 12px;
}

/* Hover now fills, matching `.nav-item` — the other control in this row. A border-colour shift
   alone left the select the one interactive thing in the header that gave no ground back. */
.category-filter:hover .category-select { border-color: var(--faint); }
.category-filter:hover .category-filter__chevron { color: var(--muted); }

/*
  The current category. This is the whole point of the polish: the select was the only place the
  header stated which category the reader was in, and it looked exactly the same whether it said
  "All topics" or the page you were standing on.

  Accent fill, matching `.nav-item.active`. It does not break the One Accent Rule — on a category
  page no nav item is active, and on every other page this control is not, so the header still
  carries at most one accent.
*/
.category-filter.is-active .category-select {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}
.category-filter.is-active .category-filter__chevron { color: var(--accent-contrast); }
.category-filter.is-active:hover .category-select {
  background: var(--accent-dim);
  border-color: var(--accent-dim);
}

.category-filter:focus-within .category-select { border-color: var(--accent-dim); }
.category-filter.is-active:focus-within .category-select { border-color: var(--accent-contrast); }

/* SPEC §15 — the category page's sort chips. The selected state is an inline style there,
   because a sort chip is a set of links rather than a single filter's state. */
.cat-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 13px;
  color: var(--muted);
  white-space: nowrap;
}
.cat-chip:hover { color: var(--text); border-color: var(--faint); }
.cat-count { color: var(--faint); font-size: 11px; font-variant-numeric: tabular-nums; }

/* --- main --- */

.main { padding: 36px 24px 72px; min-height: 60vh; }

/* --- hero --- */

.hero { padding: 20px 0 34px; border-bottom: 1px solid var(--border-soft); margin-bottom: 34px; }
.hero h1 {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}
.hero-sub {
  margin: 8px 0 0;
  font-family: var(--sans);
  font-size: clamp(30px, 6vw, 48px);
  line-height: 1.08;
  letter-spacing: -0.03em;
}
.hero-meta { margin: 12px 0 0; color: var(--faint); font-size: 13px; }

/* --- boards --- */

.board { margin-bottom: 64px; }
.board-title {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
/*
 * The glyph sits on the text baseline, not beside it.
 *
 * These two headings used to carry a colour emoji — `🔥 Trending`, `🕐 Latest` — which
 * rendered in the platform's emoji font at an optical size unrelated to the 12px uppercase
 * label and in its own palette. The nav traded exactly those for vendored Phosphor paths;
 * leaving them here, on the first screen of the site, undid that one surface at a time.
 *
 * `opacity` rather than a colour: the icon inherits `currentColor` from the heading, so it
 * follows the theme and any hover state with no rule of its own — the same contract the nav
 * and the theme switch rely on.
 */
.board-icon { flex: none; opacity: 0.8; }
.board-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
.board-more { font-size: 13px; color: var(--faint); }
.board-more:hover { color: var(--accent); }

/*
 * The board pager: the page number at the left edge, both controls at the right.
 *
 * The `auto` margin is the whole layout, and it is not the obvious spelling.
 * `justify-content: space-between` reads like the right tool and is wrong here — with
 * three children it parks them at left, centre and right, which strands the number in
 * the middle next to the newer link instead of at the edge. An auto margin pushes only
 * what follows it, so the number stays put and the controls travel together.
 *
 * Baseline rather than centre, to match `.board-head` above: the number and the links are
 * both small text, and aligning their baselines is what makes them read as one row rather
 * than as three separate floats. Sized like `.board-more` for the same reason — a pager
 * is a secondary control and should not out-shout the cards it moves between.
 */
.pager { display: flex; align-items: baseline; gap: 16px; font-size: 13px; }
.pager > :first-child { margin-right: auto; }
.pager a { color: var(--faint); }
.pager a:hover { color: var(--accent); }

.board-split { display: grid; grid-template-columns: 1fr 1fr; gap: 36px; }

.grid-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(292px, 1fr)); gap: 18px; }
/*
 * Trending and Latest, each a two-up grid inside its half of the split.
 *
 * These were one card per row, and a card in a half-width column is 667px wide — for a single
 * article that is a stretched banner, out of scale with the 367px cards in Editor's Pick
 * directly above it. Two per column puts them back at roughly the same size as every other
 * card on the page: a card is a card wherever it sits.
 *
 * `auto-fill` with a floor rather than a hard `1fr 1fr`, so that when the split itself
 * collapses to one column at 820px this becomes more cards across instead of two enormous
 * ones. The rank badge rides on the cover, so a two-up grid does not disturb it.
 */
.ranked-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(276px, 1fr));
  gap: 18px;
}

/* --- article card ---

 * SPEC §17 — a cover on top, text beneath.
 *
 * The card used to be a horizontal row with the rank number in its own left column. It is a
 * column now because the cover needs the full width to be worth showing, and because the
 * rank reads better as a numeral over the image than as a leading margin that competes with
 * it for the reader's first look.
 *
 * `overflow: hidden` is what lets the cover clip to the card's radius. Without it the image's
 * square corners sit outside the rounded border and the card looks like two stacked
 * rectangles rather than one object.
 */

.card-article {
  display: flex;
  flex-direction: column;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  overflow: hidden;
  transition: border-color 0.15s ease;
}
.card-article:hover { border-color: var(--accent-dim); }

.card-cover {
  position: relative;
  display: block;
  /*
   * A fixed-height box, filled edge to edge.
   *
   * Every card must be the same height, and the cover is the one element whose height is not
   * determined by text — so it is pinned. A fixed `height` rather than a ratio, because a
   * ratio made the box a different height in each column: measured at 205px in the two-column
   * Trending area and 260px in the wide Editor's Pick grid, so the same card was two different
   * heights depending on where it sat.
   *
   * 150px, chosen by measurement. The archived covers are wide banners — 1200x480 is typical,
   * 2.5:1 — and `cover` scales the image to the box's height and crops the overflow
   * horizontally, so the box's height *is* the crop. At 220px the image was drawn 550px wide
   * against a 365px slot and 185px (34%) was thrown away, cutting the first letter off a
   * headline. At 150px it is drawn 375px wide — 10px, 2.7%, and nothing in the wider Trending
   * column. So the shorter box shows essentially the whole picture and is still a fixed height.
   *
   * `object-fit: cover` crops rather than bars. This is a deliberate reversal of an earlier
   * `contain`: showing the whole image *and* keeping every card the same height is not
   * possible at once, and when the two conflict uniformity wins. `contain` in a fixed box left
   * 37px of grey above and below every 1200x480 image. Bars read as a layout that failed to
   * load; a cropped banner reads as a thumbnail, which is what a card cover is.
   *
   * `flex: none` keeps the box from stretching when the card does. The slack belongs to the
   * body, where the footer is pinned to the bottom.
   */
  height: 150px;
  background: var(--panel-2);
  overflow: hidden;
  flex: none;
}
.card-cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* No archived image, and no image to take a shape from.
 *
 * This has to look like the covers around it rather than like a gap, so its height is tied
 * to the shape the archived images actually are. Measured across the live homepage they run
 * about 2.4:1 — wide banners — and `aspect-ratio: 5 / 2` matches that closely enough that a
 * placeholder sits at the same height as its neighbours instead of towering over them.
 *
 * An earlier `16 / 9` was the intuitive choice and measurably wrong: at the Trending column's
 * width it produced a 307px band beside 218px covers, so the card with *no* image was the
 * tallest one in the row. A blank block claiming more space than a photograph is the one
 * error a reader is guaranteed to notice. */
.card-cover-empty {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--panel-2);
}

/*
 * The rank sits on the cover, in the accent.
 *
 * It is the one number on the card that is about *this position* rather than about the
 * article, so it is the one thing styled to stand out — and the accent is the token this
 * design system already uses for "this is the current/active thing".
 *
 * `tabular-nums` so 1 and 10 do not shift the layout, and a translucent backing because the
 * cover underneath is arbitrary: a numeral in accent on a light photograph is unreadable.
 */
.rank {
  position: absolute;
  left: 10px;
  bottom: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  padding: 2px 8px;
  border-radius: 7px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  font-family: var(--serif);
  font-size: 20px;
  line-height: 1.2;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* SPEC §15 — the language badge, in the cover's top-right, opposite `.rank` at the
   bottom-left. Same translucent pill so the two read as the same kind of annotation over
   whatever image sits behind them. Uses the sans stack rather than `.rank`'s serif because
   it carries words (`中文`, `日本語`) rather than a numeral, and CJK glyphs in the serif
   face fall back inconsistently across platforms. */
.lang-badge {
  position: absolute;
  right: 10px;
  top: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px 8px;
  border-radius: 7px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.4;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--accent);
  /* The cover image is not a predictable colour, and the pill is only mostly opaque, so a
     subtle text shadow keeps the label legible if a light image shows through. */
  text-shadow: 0 1px 2px color-mix(in srgb, var(--bg) 60%, transparent);
}

.card-body { min-width: 0; padding: 14px 16px 16px; display: flex; flex-direction: column; flex: 1; }
.card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 21px;
  font-size: 12px;
  margin-bottom: 7px;
  /*
   * One line, a fixed height, clipped rather than wrapped.
   *
   * Two things made this row's height vary: the wrap when its four items did not fit, and the
   * Editor's Pick badge — 11px in a 1px border, so a row containing it measured 21px against
   * 18px without one, letting a decoration decide the card's height. A fixed `height` with
   * `align-items: center` puts the badge in the middle of the row instead of setting it.
   *
   * The date is the first thing clipped, which is the right priority: the category and the
   * reading time are what a reader decides on, and the date is the least of the three.
   */
  white-space: nowrap;
  overflow: hidden;
  min-width: 0;
}
.tag-cat { color: var(--accent); font-weight: 600; }
.badge {
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  border: 1px solid currentColor;
}
.badge-pick { color: var(--warn); }

.card-title {
  margin: 0 0 6px;
  font-family: var(--sans);
  font-size: 18px;
  line-height: 1.3;
  letter-spacing: -0.015em;
  font-weight: 600;
  /*
   * One line, ellipsised.
   *
   * Titles in this archive run from 25 to 59 characters — real headlines, not the two- or
   * three-word ones a card layout usually assumes — so letting them wrap gave the same row
   * cards of three different heights. One line with an ellipsis makes every title one line.
   *
   * `min-width: 0` is what makes it work: the title is inside a flex column, and a flex item
   * defaults to `min-width: auto`, which refuses to shrink below its content — so without
   * this the title would widen its own column instead of truncating and no ellipsis would
   * appear. The full headline stays in the DOM; only the painted box is short.
   */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.card-title a:hover { color: var(--accent); }

.card-excerpt {
  margin: 0 0 10px;
  color: var(--muted);
  font-size: 13.5px;
  line-height: 1.55;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/*
 * A card with no summary renders nothing here, and the feet still line up.
 *
 * There used to be a `.card-excerpt-empty` that held two blank lines open so a mixed feed
 * kept one card height. It was unnecessary: `.card-article` is a flex column that the grid
 * stretches to its row's height, and `.card-foot` is pinned with `margin-top: auto`, so a
 * short card's slack already collects above the foot. What the reservation actually did was
 * put a blank band the height of a sentence on every card in the archive — all 105 published
 * articles have no summary — which is the largest empty space on the page.
 */
/*
 * Two groups, pushed apart: the author belongs to the article, the counters belong to X.
 * `margin-top: auto` on the row is what pins both to the bottom of a card whose title is
 * short, so a row of cards lines up along their feet rather than along their title lengths.
 */
.card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: auto;
  padding-top: 10px;
  font-size: 12px;
  color: var(--faint);
  /*
   * Author on the left, counters on the right, one line.
   *
   * It used to wrap, so a card whose counters did not fit pushed them onto a second line and
   * grew taller than the card beside it — the engagement row was also what broke the grid.
   * `nowrap` with hidden overflow keeps the height fixed and clips the counters from the
   * right, which is the correct thing to lose: the author is the attribution, the counters a
   * bonus. `margin-top: auto` pins the row so leftover height collects above it.
   */
  white-space: nowrap;
  overflow: hidden;
  min-width: 0;
}
.author { color: var(--muted); }

.card-metrics {
  display: flex;
  align-items: center;
  gap: 10px;
  list-style: none;
  margin: 0;
  padding: 0;
  font-variant-numeric: tabular-nums;
}

/*
 * Each counter: icon then number. `gap: 3px` so the pair reads as one unit while the pairs
 * stay separated by the row's own 10px — the two distances have to differ or six counters
 * read as twelve items.
 */
.card-metrics li { display: inline-flex; align-items: center; gap: 3px; }
.metric-icon { flex: none; opacity: 0.75; }
.metric-value { line-height: 1; }

/* Announced but not shown: the icon conveys the meaning visually and this gives it a name
 * for a screen reader, which a bare "4.6M" does not have. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.score { font-variant-numeric: tabular-nums; }
.muted { color: var(--faint); }

/* --- tag grid --- */

/*
 * The category grid that shared this heading is gone, and so are its four rules.
 * They are deleted rather than left declared: a selector nothing renders reads as live
 * and gets restyled later, and `.cat-card`/`.cat-name`/`.cat-desc` were also members of
 * the 1.5 line-height group above, so they would have lingered there as references to
 * nothing. `.cat-count` survives — the category select's option labels and this page's tag
 * cloud both still use it, and `.cat-chip` survives on the category page's sort row.
 */

.tag-cloud { display: flex; flex-wrap: wrap; gap: 9px; }
.tag {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 13px;
  color: var(--muted);
}
.tag:hover { color: var(--text); border-color: var(--faint); }

/* --- empty state --- */

.empty-state {
  padding: 72px 32px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  text-align: center;
}
.empty-state h2 { margin: 0 0 8px; font-family: var(--sans); font-size: 22px; }
.empty-state p { color: var(--muted); margin: 0 auto 16px; max-width: 52ch; }
.empty-state pre { display: inline-block; margin: 0; text-align: left; }

/* --- reading page --- */

.article-shell { max-width: 720px; margin: 0 auto; }
.article-head { margin-bottom: 36px; }
.article-crumbs { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 12px; margin-bottom: 20px; }
.article-title {
  margin: 0 0 16px;
  max-width: 24ch;
  font-family: var(--sans);
  font-size: clamp(26px, 4.8vw, 38px);
  line-height: 1.28;
  /* No negative tracking. It is tuned for Latin counters, and CJK glyphs already
     fill the em box — tracking them in squeezes the ideographs together and
     reads as a rendering fault rather than as tightening. */
  letter-spacing: 0;
  font-weight: 700;
}
.byline { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; color: var(--muted); font-size: 13px; }
.byline a { color: var(--muted); }
.byline a:hover { color: var(--accent); }

.stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  padding: 14px 16px;
  margin: 24px 0;
  border-top: 1px solid var(--border-soft);
  border-bottom: 1px solid var(--border-soft);
  font-size: 12.5px;
  color: var(--faint);
}
.stat-row span { color: var(--muted); font-size: 12px; line-height: 1.4; text-transform: uppercase; letter-spacing: 0.04em; }
.stat-row b { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; text-transform: none; letter-spacing: 0; }

/*
 * The reading surface. Sans rather than serif: a Chinese serif resolves to Songti
 * SC, whose hairline strokes go grey against a light ground and muddy against a
 * dark one. A PingFang / Noto Sans face keeps a consistent stem weight at 17.5px
 * and actually uses the 1.75 leading — the 1.72 this had was measured against
 * Latin, and Chinese glyphs are full-width and need the extra room to avoid
 * neighbouring lines appearing to touch.
 */
.article-body { font-family: var(--sans); font-size: 17.5px; line-height: 1.75; }
.article-body p { margin: 0 0 1.15em; }
.article-body h1, .article-body h2, .article-body h3 {
  font-family: var(--sans);
  letter-spacing: -0.02em;
  line-height: 1.25;
  margin: 1.8em 0 0.6em;
}
.article-body h1 { font-size: 26px; }
.article-body h2 { font-size: 21px; }
.article-body h3 { font-size: 17px; color: var(--muted); }
.article-body blockquote {
  margin: 1.5em 0;
  padding: 2px 0 2px 18px;
  border-left: 3px solid var(--accent-dim);
  color: var(--muted);
  font-style: italic;
}
.article-body ul, .article-body ol { margin: 0 0 1.15em; padding-left: 1.4em; }
.article-body li { margin-bottom: 0.4em; }
.article-body a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.article-body pre {
  margin: 1.5em 0;
  padding: 14px 16px;
  overflow-x: auto;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
}
.article-body code { font-family: var(--mono); font-size: 0.88em; }
.code-block { margin: 1.5em 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.code-block-header { padding: 8px 16px; background: var(--panel-2); border-bottom: 1px solid var(--border); font-family: var(--mono); font-size: 11px; line-height: 1.5; color: var(--muted); }
.article-body .code-block pre { margin: 0; border: 0; border-radius: 0; padding: 16px; line-height: 1.65; }
.article-body .code-block pre code { font-size: inherit; }
:root[data-theme='dark'] .shiki, :root[data-theme='dark'] .shiki span { color: var(--shiki-dark, var(--text)) !important; background-color: var(--shiki-dark-bg, var(--panel)) !important; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .shiki, :root:not([data-theme='light']) .shiki span { color: var(--shiki-dark, var(--text)) !important; background-color: var(--shiki-dark-bg, var(--panel)) !important; }
}
.article-body :not(pre) > code { background: var(--panel-2); padding: 1px 5px; border-radius: 4px; }
.article-body figure { margin: 1.8em 0; }
.article-body img { width: 100%; height: auto; border-radius: var(--radius); border: 1px solid var(--border); }

.article-footer { margin-top: 40px; padding-top: 24px; border-top: 1px solid var(--border-soft); }
.article-footer-actions { display: flex; align-items: center; gap: 16px; }
.tag-row { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 24px; }
.original-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 14px;
  font-weight: 600;
}
.original-link:hover { border-color: var(--accent-dim); background: var(--panel); }

.notice {
  padding: 22px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  margin: 24px 0;
  color: var(--muted);
}

/* --- search --- */

.search-form { display: flex; gap: 10px; margin-bottom: 28px; }
.search-form input {
  flex: 1;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 11px 15px;
  font-size: 15px;
}
.search-form input:focus { border-color: var(--accent-dim); }
.search-form button {
  background: var(--accent);
  color: var(--accent-contrast);
  border: 0;
  border-radius: var(--radius);
  padding: 11px 22px;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
}
.result-snippet { font-size: 13px; color: var(--muted); margin: 6px 0 0; }
/* A tinted accent, derived so the highlight follows the theme rather than pinning
   the old blue. color-mix keeps one rule for both themes. */
.result-snippet mark { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text); padding: 0 2px; border-radius: 3px; }

/* --- misc --- */

pre {
  font-family: var(--mono);
  font-size: 13px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 15px;
  overflow-x: auto;
}
.prose { max-width: 72ch; }
.prose h1 { font-family: var(--sans); font-size: 30px; letter-spacing: 0; margin: 0 0 12px; }
.prose h2 { font-size: 18px; margin: 32px 0 10px; }
.prose p { color: var(--muted); }
.prose ul { color: var(--muted); }

/*
 * SPEC §43 — the /account page.
 *
 * `.lede`, `.banner` and `.btn` exist in admin.css and this is their first use on the
 * public site, so they are defined here rather than shared: admin.css and site.css are
 * separate bundles, and coupling them for three rules would make the admin page pay for
 * the reading page's stylesheet. The values match admin.css so the two surfaces do not
 * drift into looking like different products.
 */

.lede { color: var(--muted); margin: 0 0 18px; max-width: 68ch; }

.banner {
  padding: 12px 16px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--panel-2);
  font-size: 14px;
  margin: 0 0 20px;
}

/* The element carries `role="status"`, so this is an announcement rather than decoration. */
.banner-warn { border-color: color-mix(in srgb, var(--warn) 45%, transparent); color: var(--text); }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }

/*
 * An accent fill, on the same token and with the same lettering as the active nav item.
 *
 * `--accent-contrast`, and not `--accent-ink`. Those are two names for two different
 * things: admin.css defines `--accent-ink` and site.css defines `--accent-contrast`, and
 * only the latter exists on this surface. Written as `--accent-ink` here, the var() had
 * nothing to resolve to and the declaration fell back to the inherited `--text` —
 * near-black lettering on a #176b75 fill, measured at **2.37:1**, which fails WCAG AA even
 * for bold text. The nav's active state was 6.18:1 at the same moment, from the token that
 * actually exists.
 *
 * So this was not only a mismatch between two surfaces, it was a contrast bug that looked
 * like a styling choice. Hover keeps the same lettering and lifts the fill, because
 * brightening a background does not change which colour has to sit on top of it.
 */
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
  font-weight: 600;
}
.btn-primary:hover { color: var(--accent-contrast); filter: brightness(1.08); }

.account { max-width: 62ch; }
.account h2 { margin-top: 28px; }

.account-id { display: flex; align-items: center; gap: 14px; margin: 20px 0 4px; }
.account-avatar { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; flex: none; }
.account-name { margin: 0; font-size: 18px; font-weight: 600; color: var(--text); }
.account-email { margin: 2px 0 0; font-size: 14px; color: var(--muted); }

.account-idents, .account-providers { list-style: none; padding: 0; margin: 0 0 20px; }
.account-idents li, .account-providers li { padding: 8px 0; border-bottom: 1px solid var(--border); color: var(--text); }
.account-idents li:last-child, .account-providers li:last-child { border-bottom: none; }
.account-providers li { border-bottom: none; padding: 6px 0; }

/*
 * SPEC §17.1 — the author index.
 *
 * A grid of links rather than cards with a thumbnail: the avatar is the affordance, not
 * the content, and these entries have no title or excerpt to make a card out of. Two
 * columns at most, because a row of names is easier to scan than a wall of them and the
 * name lengths here vary a lot.
 */
.author-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
  list-style: none;
  padding: 0;
  margin: 0;
}

.author-card { min-width: 0; }

.author-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  min-width: 0;
}

.author-link:hover { border-color: var(--accent-dim); }

.author-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex: none;
  object-fit: cover;
}

/*
 * The stand-in when there is no picture. Same treatment as the header slot's letter —
 * accent fill, `--accent-contrast` lettering — so a missing avatar reads as the same kind
 * of thing on both surfaces rather than as two different failures.
 */
.author-avatar-letter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
}

.author-meta { display: flex; flex-direction: column; min-width: 0; gap: 2px; }

/* Both truncate rather than wrap: one long X display name must not make its row taller
 * than the others in the same grid row. */
.author-display {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.author-handle {
  color: var(--faint);
  font-size: 12.5px;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-footer { border-top: 1px solid var(--border); padding: 28px 0; margin-top: 40px; }
.footer-inner { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--faint); }
.footer-inner p { margin: 0; }
.footer-links { display: flex; gap: 16px; }
.footer-links a { color: var(--muted); }

/*
 * The header row runs out of room before the nav does, and what it gives up is decided
 * in that order: the site name, then the search box, then the nav glyphs.
 *
 * Every threshold below is measured, not chosen. With seven items at 10px padding the nav
 * needs 637px with glyphs and 418px without, and the row has 642px at 1200px, 613px at
 * 1151px, 610px at 1024px and 626px at 820px — the gaps jump around because the search box
 * disappearing at 820px hands its width straight back.
 *
 * The order matters. Primary navigation goes before everything else: losing a nav item to
 * a horizontal scroll with no affordance is losing the site, whereas the site name is
 * carried by the mark and the document title, and the search box is a shortcut to a page
 * the nav links to directly. Below about 700px the row does scroll, which is what it did
 * with six items too — the phone case was never a fitting case.
 */
/*
 * The header's spacing is now set once, at the base rule: the site name is drawn at no
 * width at all, so there is no band where the row has more room and can afford a wider gap.
 *
 * The block that used to live here hid the site name at 1300px. It is gone for the reason
 * given at `.logo-text` — the category filter moved into this row, and the column is
 * `--wrap`-capped at every width above it, so a wider monitor buys nothing.
 */

/*
 * The search box goes next, for the same kind of reason: it is a shortcut to a page the
 * nav lists.
 *
 * 1100px was the old threshold, 1200px the one after it, and both reason about a ~190px
 * box. The control is now a pair — the category filter sits beside the search field inside
 * the same `.search-mini` wrapper — at roughly 350px, so this threshold is the one that
 * absorbed the filter rather than the nav having to.
 *
 * Re-measured with the Chinese labels, which are the widest of the three: with the filter
 * shown the nav needs 372px, and the row at 1201px leaves it 424px. At 1200px and below the
 * pair is gone and the nav gets the whole row, which is why the threshold is here and not
 * lower.
 */
@media (max-width: 1200px) {
  .search-mini { display: none; }
}

@media (max-width: 900px) {
  .nav-icon { display: none; }
}

/*
 * Narrow rows: 13px labels. Same 9px sides as the base rule above, so this override is only
 * about the type size now.
 *
 * 9px is what brings 700px inside the line and was already the value here. It is also what
 * a 640px row needs: measured with the sixth item, six labels at 14px/10px needed 381px
 * against 378px available — 3px over, and a silent 3px scroll is exactly the overflow that
 * reads as a rendering quirk rather than as a layout fault.
 *
 * Below about 560px six items plus the mark and the theme switch genuinely do not fit, and
 * the nav scrolls — on a phone that is the normal behaviour for a row this long.
 */
@media (max-width: 700px) {
  /* 8.5px sides and 13px labels.
   *
   * The padding is not a round number because the row is not. Measured with the sixth item,
   * a 640px viewport leaves 378px for a nav needing 381px at 9px sides — 3px over — and the
   * next round step down (8px) is more than that row needs. Half a pixel a side across six
   * items is 6px back, which closes the 3px with room to spare at every width in the band.
   *
   * This is the same lever the base rule uses, just tighter: a too-wide row is only ever
   * fixed by giving the nav's items less padding, never by narrowing the nav itself, because
   * a clipped nav item is invisible while a tighter one is merely tighter. */
  .nav-item { padding: 6px 8.5px; font-size: 13px; }
}

@media (max-width: 820px) {
  .board-split { grid-template-columns: 1fr; gap: 32px; }
  .header-inner { gap: 12px; }
}

@media (max-width: 560px) {
  /* Below the width where the row can fit the nav at all, 8px sides buy one more label
   * before the scroll starts — and the nav scrolls here regardless, which is the phone
   * behaviour this row has always had. */
  .nav-item { padding: 6px 8px; font-size: 13px; }
}

/* --- byline (SPEC §17.2) --- */

.byline { align-items: center; }
.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--border);
  object-fit: cover;
  flex: 0 0 auto;
  background: var(--panel-2);
}
.author-name { display: inline-flex; flex-direction: column; line-height: 1.3; }
.author-name .handle { color: var(--faint); font-size: 12.5px; }

/* --- figures --- */

.article-body figure.cover {
  margin: 0 0 28px;
}
.article-body figure.cover img { border-color: var(--border-soft); }
.article-body figcaption {
  margin-top: 8px;
  font-family: var(--sans);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--faint);
  text-align: center;
}

/* --- archived media (SPEC §10) --- */

/* An image the media pipeline owns: sized from the stored dimensions so the
   article does not reflow as figures arrive. */
.article-body figure.fig img {
  max-width: 100%;
  height: auto;
}

/* An image that is still on X's CDN. Deliberately not a hotlinked <img> — a
   reader deserves to know the figure is not part of the archive before it
   silently breaks, and before their browser phones x.com for it. */
.article-body figure.fallback {
  margin: 0 0 28px;
  border: 1px dashed var(--border);
  border-radius: 6px;
  background: var(--panel-2);
}
.fig-missing {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 22px 20px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted);
  text-decoration: none;
}
/*
 * `--text`, and not `--ink` — that name belongs to admin.css and this stylesheet has never
 * defined it. The var() compiled, deployed and rendered; it simply resolved to nothing, so
 * this rule was a no-op and the link stayed `--muted` on hover like every other link on
 * the page. Invisible, and harmless here, which is the worst combination: it would have
 * survived review indefinitely. See the unresolved-token assertion in typography.test.ts.
 */
.fig-missing:hover { color: var(--text); }
.fig-missing span {
  font-size: 12px;
  color: var(--faint);
}

/* Video streams from the original host; the link remains available as a fallback. */
.fig-video { margin: 20px 0; }
/*
 * An embedded post reads as a quoted post, not as a pull-quote from this article.
 * The generic .article-body blockquote is italic, accent-ruled and margin-only, which
 * made an embedded tweet look like the author quoting somebody. This is its own
 * surface: neutral border, identity row, and the action strip X shows under a post.
 */
.article-body .embedded-post {
  margin: 28px 0;
  padding: 16px 18px 12px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
}
/* Every rule below carries the `.article-body` prefix: it is what the block is
   rendered inside, and it is what the body's own `a`, `img` and `figure` rules are
   written against, so a bare class here loses to them and the avatar comes out at
   full width with the brand link underlined. */
.article-body .embedded-post__head { display: flex; align-items: center; gap: 10px; }
.article-body .embedded-post__avatar {
  flex: none;
  width: 40px;
  height: 40px;
  border: 0;
  border-radius: 50%;
  object-fit: cover;
  background: var(--panel-2);
}
.article-body .embedded-post__identity { flex: 1; min-width: 0; }
/* One row, the way X prints it: name, seal, @handle, date. Two rows repeated the
   handle and read as two different people. */
.article-body .embedded-post__name { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; font-size: 15px; line-height: 1.35; }
.article-body .embedded-post__name strong { color: var(--text); font-weight: 700; }
.article-body .embedded-post__verified { flex: none; color: var(--verified); }
.article-body .embedded-post__handle,
.article-body .embedded-post__date { color: var(--muted); }
.article-body .embedded-post__dot { color: var(--faint); }
.article-body .embedded-post__brand {
  flex: none;
  display: inline-flex;
  align-items: center;
  color: var(--faint);
  text-decoration: none;
}
.article-body .embedded-post__brand:hover { color: var(--muted); }
.article-body .embedded-post__text { margin-top: 10px; color: var(--text); font-size: 17px; line-height: 1.6; white-space: pre-wrap; }
.article-body .embedded-post__more { display: inline-block; margin-top: 4px; color: var(--accent); font-size: 15px; line-height: 1.5; text-decoration: none; }
.article-body .embedded-post__more:hover { text-decoration: underline; text-underline-offset: 3px; }
/* A post quoted inside a quoted post: X draws it as a quieter card within the first
   one, with its media running to that card's own edges. */
.article-body .embedded-quote {
  margin: 14px 0 0;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel-2);
}
.article-body .embedded-quote__head { display: flex; align-items: center; gap: 8px; padding: 12px 12px 0; }
.article-body .embedded-quote__avatar {
  flex: none;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 50%;
  object-fit: cover;
  background: var(--panel);
}
.article-body .embedded-quote__name { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; min-width: 0; font-size: 14px; line-height: 1.35; }
.article-body .embedded-quote__name strong { color: var(--text); font-weight: 700; }
.article-body .embedded-quote__text { padding: 8px 12px 12px; color: var(--text); font-size: 15px; line-height: 1.6; white-space: pre-wrap; }
.article-body .embedded-quote__media { display: block; background: var(--panel); text-decoration: none; }
.article-body .embedded-quote__media img { display: block; width: 100%; height: auto; border: 0; border-radius: 0; }
/* A quoted X Article inside a quoted post: cover and badge on the left, its own
   title and excerpt on the right — the shape the capture had flattened into a
   single Markdown link. */
.article-body .embedded-card {
  display: grid;
  grid-template-columns: 116px 1fr;
  gap: 12px;
  margin-top: 14px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel-2);
  color: var(--text);
  text-decoration: none;
}
.article-body .embedded-card:hover { border-color: var(--accent-dim); }
.article-body .embedded-card__cover { position: relative; display: block; overflow: hidden; border-radius: 8px; background: var(--panel); }
.article-body .embedded-card__cover img { display: block; width: 100%; height: 100%; border: 0; border-radius: 0; object-fit: cover; }
.article-body .embedded-card__label {
  position: absolute;
  left: 6px;
  bottom: 6px;
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--app-chip-bg);
  color: var(--app-chip-ink);
  font-size: 10px;
  line-height: 1.5;
  font-weight: 700;
}
.article-body .embedded-card__body { display: flex; flex-direction: column; justify-content: center; gap: 4px; min-width: 0; }
.article-body .embedded-card__title { font-size: 14px; line-height: 1.35; font-weight: 700; overflow-wrap: anywhere; }
.article-body .embedded-card__excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}
.article-body .embedded-post__video { display: block; width: 100%; max-height: 70vh; margin-top: 14px; border-radius: 10px; background: var(--bg); }
/* A post's own pictures. One stands alone; the rest tile, with an odd one out given
   the tall half of the grid the way X lays a three-photo post out. */
.article-body .embedded-post__media {
  display: grid;
  gap: 2px;
  margin-top: 12px;
  overflow: hidden;
  border-radius: 12px;
  background: var(--bg);
}
.article-body .embedded-post__media > a { display: block; min-width: 0; text-decoration: none; }
.article-body .embedded-post__media img { display: block; width: 100%; height: 100%; border: 0; border-radius: 0; }
.article-body .embedded-post__media--single img { height: auto; max-height: 70vh; object-fit: contain; }
.article-body .embedded-post__media--multi { grid-template-columns: 1fr 1fr; }
.article-body .embedded-post__media--multi img { aspect-ratio: 1; object-fit: cover; }
.article-body .embedded-post__media--odd > :first-child { grid-row: span 2; }
.article-body .embedded-post__media--odd > :first-child img { aspect-ratio: auto; height: 100%; }
.article-body .embedded-post__actions { display: flex; align-items: center; gap: 22px; margin-top: 14px; color: var(--faint); font-size: 13px; line-height: 1.4; }
.article-body .embedded-post__actions span { display: inline-flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; }
.article-body .embedded-post__actions > a { margin-left: auto; color: var(--accent); text-decoration: none; }
.article-body .embedded-post__actions > a:hover { text-decoration: underline; text-underline-offset: 3px; }
/*
 * An x.ai plugin link is an app, not a citation: X shows its own Open Graph
 * banner as a card with the plugin title on it. The banner is served by x.ai
 * and cached at parse time, so the reading page makes no request to render it.
 */
.article-body .app-card { margin: 28px 0; }
.app-card__banner {
  position: relative;
  display: block;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel-2);
}
.app-card__banner img { display: block; width: 100%; height: auto; border: 0; border-radius: 0; }
.app-card__chip {
  position: absolute;
  left: 16px;
  bottom: 16px;
  max-width: calc(100% - 32px);
  padding: 4px 10px;
  border-radius: 8px;
  background: var(--app-chip-bg);
  color: var(--app-chip-ink);
  font-size: 13px;
  line-height: 1.4;
}
.app-card__banner--plain { display: flex; align-items: center; min-height: 84px; padding: 16px; }
.app-card__banner--plain .app-card__chip { position: static; background: var(--panel); color: var(--text); }
.app-card__caption { margin-top: 8px; }
.app-card__caption a { color: var(--muted); font-size: 13px; line-height: 1.4; text-decoration: none; }
.app-card__caption a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.fig-video video { display: block; width: 100%; max-height: 80vh; background: var(--bg); border-radius: 8px; margin-bottom: 10px; }
.fig-video span { display: block; font-size: 12px; line-height: 1.5; color: var(--faint); }

.avatar.is-remote { opacity: 0.75; }

/* --- theme switcher -------------------------------------------------------
 *
 * A three-state segmented control rather than a single toggle, because "follow the
 * system" is a real third state and a toggle cannot express it: flipping back to
 * light would be indistinguishable from choosing light, so a reader whose OS
 * switches to dark at sunset would need to touch the control twice every evening.
 *
 * Radios rather than buttons so the current choice is in the form state, which is
 * what lets the CSS highlight the selected segment without JavaScript and keeps it
 * keyboard-navigable. The fieldset is `display: contents` so it does not add a box.
 */

/*
 * SPEC §43 — the header's account slot.
 *
 * What is in it: the fixed string "Sign in", or a fixed-size avatar. Nothing in the slot
 * is of variable length any more, which is why it carries no width bound of its own.
 *
 * That used to need one. While the display name was rendered here, the slot had to be
 * capped (`max-width`) and had to narrow again at the same 700px the nav labels narrow at,
 * because a long name would otherwise take its width straight out of the nav — `.nav` is
 * `flex: 1`, so it absorbs the pressure and starts scrolling, silently, with no
 * affordance. Measured at the time: at 640px a 36-character name left the nav 22px short.
 *
 * Removing the name removed the pressure, so the cap and the narrowing rule went with it
 * rather than being left as defence against nothing. The invariant they were protecting is
 * now structural — there is no variable-width content in the slot to protect it.
 */

.auth-slot {
  display: inline-flex;
  align-items: center;
  flex: none;
}

.auth-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 10px;
  border-radius: 7px;
  color: var(--muted);
  font-size: 14px;
  white-space: nowrap;
}

.auth-link:hover { color: var(--text); background: var(--panel-2); }

/*
 * SPEC §43 — the account menu.
 *
 * `position: relative` on the slot is what the menu is anchored to, so it opens under the
 * avatar rather than under the page. `right: 0` because the slot sits at the right edge of
 * the header: a menu aligned to its left edge would run off the viewport on a narrow one.
 *
 * The shadow is the only elevation in this stylesheet. A menu has to read as being above
 * the page rather than part of it, and the border alone is not enough where it overlaps the
 * header's own bottom border.
 */
.auth-slot { position: relative; }

.auth-trigger { border: 0; background: none; cursor: pointer; font: inherit; }
.auth-trigger[aria-expanded='true'] { color: var(--text); background: var(--panel-2); }

.auth-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 160px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-menu);
  /* Above the header's sticky/fixed siblings and the article body's own stacking. */
  z-index: 50;
}

.auth-menu[hidden] { display: none; }

.auth-menu-item {
  display: block;
  padding: 8px 10px;
  border-radius: 7px;
  color: var(--text);
  font-size: 14px;
  white-space: nowrap;
}

.auth-menu-item:hover { background: var(--panel-2); color: var(--accent); }

/*
 * SPEC §16 — the admin entry sits apart from the reader's own two.
 *
 * Saved and Account are the reading site; Admin leaves it entirely, for a different
 * interface with different rules. A rule above it is the smallest thing that says so, and
 * it avoids the alternative of an accent colour, which on this site means "selected" — the
 * wrong idea for a door rather than a state.
 */
.auth-menu-item-admin {
  margin-top: 6px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
  border-radius: 0 0 7px 7px;
}

/*
 * SPEC §43 — the save control on an article.
 *
 * `margin-left: auto` inside the crumbs row, which is a flex container, so it sits at the
 * far right rather than beside "Latest" — the crumbs are navigational and this is an action,
 * and grouping them would read as a third crumb.
 *
 * The saved state uses the accent, the same token the active nav item and the primary button
 * use, so "this is on" means one thing on this site.
 */
.save-slot { margin-left: auto; display: inline-flex; align-items: center; }

.save-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  line-height: 1.5;
  cursor: pointer;
  white-space: nowrap;
}

.save-button:hover { border-color: var(--accent-dim); color: var(--text); }

.save-button.is-saved {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
  font-weight: 600;
}

/* The bottom slot is not in a flex row, so it needs its own spacing rather than `auto`. */
.article-foot .save-slot { margin-left: 0; margin-right: auto; }

.auth-avatar {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  flex: none;
  object-fit: cover;
}

/*
 * A letter standing in for a missing avatar, on the accent so it reads as the same kind
 * of thing as a picture rather than as an empty box. `line-height: 1` and the flex
 * centring stop the glyph from sitting low in the circle — without them the character
 * inherits the 1.5 fallback from above and reads as off-centre.
 */
.auth-avatar-letter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
  text-transform: none;
}

.theme-switch {
  display: contents;
}

.theme-switch__options {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-2);
  flex: 0 0 auto;
}

.theme-switch__option {
  position: relative;
  display: grid;
  place-items: center;
  width: 30px;
  height: 26px;
  border-radius: 6px;
  cursor: pointer;
  color: var(--faint);
  transition: color 0.12s ease, background-color 0.12s ease;
}

.theme-switch__option:hover { color: var(--muted); }

.theme-switch__option:has(input:checked) {
  color: var(--accent-contrast);
  background: var(--accent);
}

/* The focusable element here is a visually-hidden radio, so the standard ring would
   land on it and be invisible. This redirects the ring onto the visible option the
   reader can actually see. Offset matches the global standard for the same reason
   that one is inset: an outset ring would be clipped by the switch's own bounds. */
.theme-switch__option:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}

/* Visually hidden but still focusable and still read by assistive tech. A
   `display: none` input would drop the radio group out of the tab order. */
.theme-switch__option input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

/*
 * The glyph is a filled Phosphor path, so this rule carries no `fill` and no `stroke`.
 *
 * Both were here for the hand-drawn 24-grid icons that used to sit in this control,
 * and leaving them behind would fail silently: a CSS declaration beats the SVG
 * `fill="currentColor"` presentation attribute, so `fill: none` with no stroke renders
 * all three options as blank squares inside a still-clickable radio group.
 *
 * Size is not set here either — `Icon.astro` takes it, so the box the browser reserves
 * before this stylesheet loads is already the real one.
 */
.theme-switch__icon {
  /* The input covers the whole label, so the icon sits on top of the target; without
     this it would swallow the pointer and the hit area would stop at the glyph. */
  pointer-events: none;
}

/*
 * SPEC §17.4 — the language switcher, beside the theme control in the header.
 *
 * Buttons rather than radios: the choice is applied by script and there is nothing for a
 * form to submit to, so a radio group's semantics would be a form that never posts. The same
 * segmented-control box as the theme switch, because the two sit next to each other and a
 * second visual language for the same kind of control would read as two different things.
 */
.lang-switch {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-2);
  flex: 0 0 auto;
}

.lang-switch__option {
  display: grid;
  place-items: center;
  min-width: 30px;
  height: 26px;
  padding: 0 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  /* The sans stack, not the serif: these render 中文 / 日本語 glyphs, and the serif face
     falls back inconsistently for CJK across platforms. */
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  color: var(--faint);
  cursor: pointer;
  transition: color 0.12s ease, background-color 0.12s ease;
}

.lang-switch__option:hover { color: var(--muted); }

/* `.is-active` is written by LANG_SCRIPT on load and on every click, so the highlight
   follows the applied language rather than a server-rendered guess. Without script nothing
   is marked — the page is complete in English and the control is inert, which is the
   documented degradation. */
.lang-switch__option.is-active {
  color: var(--accent-contrast);
  background: var(--accent);
}

.lang-switch__option:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}

@media (prefers-reduced-motion: reduce) {
  .theme-switch__option { transition: none; }
}

/* Compact on narrow screens: the header already carries a search box and six nav
   items, and the switch is the first thing that should give up width. */
@media (max-width: 720px) {
  .theme-switch__option { width: 27px; }
}

/* Hidden visually, still available to a screen reader.
   `display: none` and `visibility: hidden` would remove the element from the
   accessibility tree, which for a <legend> also strips the fieldset of its
   accessible name — the control would announce as an unlabelled group. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}


/* --- the homepage hero ---------------------------------------------- */

.hero {
  position: relative;
  isolation: isolate;
  padding: 72px 0 64px;
  border-bottom: 1px solid var(--border);
  background: url("/hero-library.jpg") center right / cover no-repeat;
  color: var(--text);
}
/* neutral scrim — the one permitted gradient, the same 82% paper veil the rank
   numeral already uses, only stretched across the text column. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--bg) 88%, transparent) 0%,
    color-mix(in srgb, var(--bg) 76%, transparent) 44%,
    color-mix(in srgb, var(--bg) 12%, transparent) 72%,
    color-mix(in srgb, var(--bg) 0%, transparent) 100%
  );
}
.hero > * { position: relative; z-index: 1; }

.hero-body { min-width: 0; max-width: 560px; }

.hero-title {
  margin: 0 0 16px;
  font-family: var(--sans);
  font-size: clamp(36px, 5.2vw, 64px);
  line-height: 1.0;
  letter-spacing: -0.035em;
  font-weight: 400;
  color: var(--text);
}
.hero-line {
  margin: 0 0 22px;
  font-size: 16px;
  line-height: 1.75;
  color: var(--muted);
  max-width: 42ch;
}
.hero-cta {
  display: inline-block;
  margin-top: 4px;
  color: var(--accent);
  text-decoration: none;
  font-weight: 500;
}
.hero-cta:hover { text-decoration: underline; text-underline-offset: 3px; }

.hero-credit {
  margin: 32px 0 0;
  font-size: 10px;
  line-height: 1.4;
  letter-spacing: 0.04em;
  color: var(--muted);
  opacity: 0.75;
}
.hero-credit a { color: inherit; text-decoration: none; }
.hero-credit a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }

@media (max-width: 760px) {
  .hero { padding: 48px 0 48px; background-position: 70% center; }
  .hero::before {
    background: linear-gradient(
      180deg,
      color-mix(in srgb, var(--bg) 92%, transparent) 0%,
      color-mix(in srgb, var(--bg) 74%, transparent) 52%,
      color-mix(in srgb, var(--bg) 0%, transparent) 100%
    );
  }
  .hero-body { max-width: none; }
  .hero-title { font-size: clamp(30px, 8vw, 44px); max-width: none; }
}

