/* Article page — the handful of rules the captured stylesheet has no class for.
   Hand-maintained, not emitted by the port pipeline. Imported by the /blog/<slug>
   pages alone (see astroPage in tools/04-generate.mjs); nothing else on the site
   has an article body on it.

   src/styles/blog-post.css is frozen Tailwind output, so anything the capture
   did not emit a class for has to live here rather than as a utility. */

/* ---- Table of contents ----------------------------------------------------
   Pairs with src/lib/blog-toc.js, which only flips aria-expanded. Collapsing
   is a grid-row transition rather than `display:none` so the card animates;
   `min-height:0` on the inner track is what lets the row actually reach 0. */
#contents > div[id='toc-content'] {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 200ms var(--ease-out-spring, ease), opacity 200ms ease;
}

#contents > div[id='toc-content'] > * {
  min-height: 0;
  overflow: hidden;
}

#contents button[aria-expanded='false'] + div[id='toc-content'] {
  grid-template-rows: 0fr;
  opacity: 0;
  margin-top: 0;
}

/* The capture's chevron is a glyph, not an icon font, so it rotates rather
   than swapping. Expanded points up; collapsed points back down. */
#contents button[aria-expanded='false'] .icon-glyph-11 {
  display: inline-block;
  transform: rotate(180deg);
}

#contents button {
  cursor: pointer;
}

/* ---- Wide figures ---------------------------------------------------------
   Charts and published data tables are the substance of these posts, and the
   prose column is 12 of the article grid's 24 columns — a fifteen-column TxDOT
   export in 645px is a scroll box with three columns showing.

   From xl (1140px, where the article picks up its sidebar layout) they extend
   right into the rail that holds nothing below the breadcrumb, taking them
   from 12 columns to 18.

   The 150% is exact rather than approximate. The column is 12c + 11g wide, so
   c = (P - 11g)/12, and the six columns plus six gutters to its right come to
   6c + 6g = P/2 + g/2. Deriving it from the column's own width means it holds
   at every viewport without repeating the grid formula.

   Published CSV exports take it unconditionally — fifteen columns never fit. A
   table written in the page body normally belongs in the prose column with the
   prose, which is also where cursor.com's own posts keep theirs, so it takes
   the rail only when it is measurably too wide for the column: `data-wide` is
   set by src/lib/blog-table.js, never rendered.

   Chart embeds (`figure:has(iframe)`) took the rail too until 2026-09-14 and
   came off it at the owner's direction: a chart is a fixed-ratio canvas, not a
   wide table, and at 150% it ran past the column and read as overflowing. Only
   tables may leave the prose column. */
@media (min-width: 1140px) {
  .prose figure[data-table='csv'],
  .prose figure[data-wide] {
    width: calc(150% + (0.5 * var(--spacing-g1)));
    max-width: none;
  }
}

/* ---- Tables ---------------------------------------------------------------
   The markdown-table treatment cursor.com's posts use: no frame, no vertical
   rules, a hairline above each body row, and a small uppercase header carrying
   no fill of its own. The capture emitted no `table` rules at all, so every
   rule the element needs is here.

   Markup comes from tableFigure in tools/lib/notion-html.mjs, which also mints
   the `data-` attributes these rules key off. `src/lib/blog-table.js` adds the
   fade and the button, and the clip height as `--table-clip`. */
/* The frame, not the figure: the fade covers the cut in the table and must
   stop short of the button and the caption below it. */
.prose-table__frame {
  position: relative;
}

.prose-table__scroll {
  /* Horizontal only. A clipped table has a max-height and no way to scroll
     past it; that is the point of the button below it. */
  overflow-x: auto;
  overflow-y: hidden;
}

.prose-table table {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

/* An export is wider than the column it sits in, so it takes its natural width
   and scrolls; a hand-written table fills the column. */
.prose-table[data-table='csv'] table {
  width: max-content;
  min-width: 100%;
  font-size: var(--text-xs);
}

.prose-table th,
.prose-table td {
  padding: 0.6875rem 0.875rem;
  text-align: left;
  vertical-align: middle;
}

.prose-table th:first-child,
.prose-table td:first-child {
  padding-left: 0;
}

.prose-table th:last-child,
.prose-table td:last-child {
  padding-right: 0;
}

.prose-table thead th {
  padding-block: 0.375rem;
  vertical-align: bottom;
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-theme-text-tertiary);
  /* No rule of its own: the first body row's top border is the header rule,
     which is what keeps the header reading as a label rather than a band. */
  background: var(--color-theme-bg);
  /* Only bites once a long table is expanded into its scroll frame, where the
     column names would otherwise leave with the first screenful. */
  position: sticky;
  top: 0;
  z-index: 1;
}

.prose-table tbody tr {
  border-top: 1px solid var(--color-theme-border-02);
}

/* Measurements line up on tabular figures. */
.prose-table [data-num] {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Fifteen columns of export in a 645px column wrap into unreadable stacks;
   one line per cell and a horizontal scroll is the readable trade. A body
   table has to wrap instead — holding six columns to one line each is what
   pushes it out of the prose column and into a scrollbar of its own. Values
   like "$940.49M" carry no space, so they stay whole either way. */
.prose-table[data-table='csv'] th,
.prose-table[data-table='csv'] td {
  white-space: nowrap;
}

/* ---- Long tables ----------------------------------------------------------
   Clipped mid-row with a fade over the cut and a button below it, rather than
   run out to their full length — an export is 581 rows and would otherwise be
   most of the article. Everything here is applied by src/lib/blog-table.js;
   with JS off no attribute is set, no button exists and the table renders
   whole, which is also what a crawler sees. */
.prose-table[data-clipped] .prose-table__scroll {
  max-height: var(--table-clip);
}

/* Expanded, a table past a few dozen rows gets a scroll frame instead of the
   full run — "show more" should not turn one figure into ten screens. */
.prose-table[data-framed] .prose-table__scroll {
  max-height: 32rem;
  overflow-y: auto;
}

.prose-table__fade {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 5rem;
  pointer-events: none;
  background: linear-gradient(to top, var(--color-theme-bg), transparent);
}

/* The button and the caption share one row under the table — they are the same
   kind of thing, both about the rows the reader cannot see. The figure becomes
   the grid rather than a wrapper around the two, because a `figcaption` has to
   stay a direct child of its `figure`. Only when there is a button: without one
   the caption keeps the capture's own flow. */
.prose-table:has(.prose-table__actions) {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  row-gap: var(--spacing-g1);
  column-gap: 0.875rem;
}

.prose-table:has(.prose-table__actions) .prose-table__frame {
  grid-column: 1 / -1;
}

.prose-table__actions {
  display: flex;
}

/* Same clearance the grid's row-gap gives the button, for a table short enough
   not to have one — the capture's figcaption brings no top margin of its own. */
.prose-table:not(:has(.prose-table__actions)) figcaption {
  margin-top: var(--spacing-g1);
}

.prose-table__more {
  cursor: pointer;
  border: 1px solid var(--color-theme-border-02);
  border-radius: 9999px;
  padding: 0.5rem 1rem;
  background: transparent;
  color: var(--color-theme-text-sec);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  transition:
    color 150ms ease,
    border-color 150ms ease,
    background-color 150ms ease;
}

.prose-table__more:hover {
  color: var(--color-theme-text);
  border-color: var(--color-theme-text-tertiary);
  background: var(--color-theme-card-hover-hex);
}

/* ---- Embeds ---------------------------------------------------------------
   Chart embeds carry their own background; the frame should not show through
   while one is still loading. */
.prose figure iframe {
  background: var(--color-theme-card-02-hex);
}

/* Interactive charts need room for controls and readouts that stack below the
   plot. Phones use the portrait companion to the 4:3 article canvas; videos
   keep their 16:9 inline ratio from the renderer above. */
.prose figure iframe[data-interactive-chart] {
  aspect-ratio: 3 / 4;
}

@media (min-width: 640px) {
  .prose figure iframe[data-interactive-chart] {
    aspect-ratio: 4 / 3;
  }
}

/* ---- Subscribe card -------------------------------------------------------
   Pairs with tools/fragments/article-subscribe.mjs and src/lib/subscribe-dialog.js.
   The plate is the capture's own `.card`; these three rules lay it out as one
   row — the text growing, the button pinned right — and stack it on a phone.
   The button is the capture's `.btn`, sized like the dialog's own. */
.as-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-g2);
}

.as-card__text {
  min-width: 0;
}

.as-card__title {
  margin: 0;
  font-weight: var(--font-weight-medium);
}

.as-card__line {
  margin: 0.25rem 0 0;
}

.as-card__btn {
  flex: none;
  /* The site's own button size — blog-post.css's `.btn` drops both. */
  font-size: var(--text-base);
  padding: var(--button-padding-default);
}

@media (max-width: 599px) {
  .as-card {
    flex-direction: column;
    align-items: stretch;
  }
}
