/* Layout tokens. Separate from the palette, which arrives per-site in
   the page's <head>: these describe proportion rather than colour, and
   nothing in the manifest sets them. A site that wants a different
   measure overrides one custom property in its own stylesheet instead
   of restating a layout. */
:root {
  /* Two widths, and they are two because they answer different
     questions. `--measure` is how wide a *line of prose* may get: 42rem
     is about 75 characters, the top of the range where the eye still
     finds the next line reliably. `--shell` is how wide the page's
     *furniture* may get — the header row, the footer, the band every
     section is centred in.
     
     Conflating them is what the header used to pay for. When the body
     carried one width, narrowing the measure narrowed the header with
     it, and a row of title, tabs and a search field is not a paragraph:
     it has a natural width of its own and cramps at a width that reads
     beautifully as text. */
  --measure: 42rem;          /* a line of prose */
  /* Six rems of slack over the measure — enough that the header row has
     air around its three items, and little enough that the prose still
     reads as centred under it rather than beside it. */
  --shell: 48rem;            /* the band the page is centred in */
  --toc: 16rem;              /* a book's table of contents */
  --gutter: clamp(1rem, 0.6rem + 2vw, 2rem);
  --gap: clamp(1.5rem, 1rem + 2.5vw, 2.5rem);
  --radius: 8px;
  --radius-sm: 4px;

  /* Fluid type. The bounds matter more than the slope: the small end is
     the smallest a heading may get on a phone, the large end the point
     past which a bigger screen stops making text bigger and starts
     making it silly. */
  --size-h1: clamp(1.75rem, 1.45rem + 1.5vw, 2.25rem);
  --size-h2: clamp(1.25rem, 1.13rem + 0.6vw, 1.5rem);
  --size-h3: clamp(1.05rem, 1rem + 0.3vw, 1.2rem);
  --size-small: 0.875rem;
  --size-tiny: 0.78rem;
}

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  /* Anchors land clear of the sticky-ish header rather than under it,
     and a jump lands softly unless the reader asked otherwise (below). */
  scroll-behavior: smooth;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
  line-height: 1.6;
  color: var(--text);
  /* Explicit, not inherited from the user agent. Without it a light
     palette renders its dark text straight onto a dark-mode browser's
     own dark canvas — unreadable, and invisible to anyone building on a
     machine set to light. */
  background: var(--surface);
  /* No width here. The body is the canvas; the bands below are what
     have widths, so that each can have its own. */
  /* Form controls and the like pick up the palette's accent rather than
     the platform's blue. */
  accent-color: var(--accent);
  -webkit-text-size-adjust: 100%;
}

/* Keyboard focus, everywhere, once. The rule this replaces set
   `outline: none` on the search field and offered a border colour in
   its place — which is invisible against several palettes and gone
   entirely in forced-colours mode. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

header nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  padding-block: 1.25rem;
  border-block-end: 1px solid var(--rule);
  margin-block-end: 2.5rem;
}

.site-title {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--text);
  text-decoration: none;
}

/* A flex row with a gap, rather than a margin on every child. The old
   rule put `margin-left` on each link, which meant the row could not
   wrap without the first item of each new line being indented. */
.nav-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
}

.nav-links a {
  color: var(--text-muted);
  text-decoration: none;
}

.nav-links a:hover {
  color: var(--text);
}

/* The one band, applied three times. Header, content and footer share
   `--shell` so their edges line up exactly — including in a guide book,
   which redefines the token rather than opting out of the rule. */
header nav,
main,
footer {
  max-inline-size: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.link-previews {
  list-style: none;
  padding: 0;
}

.link-preview {
  display: flex;
  align-items: start;
  gap: 1rem;
  padding-block: 1.25rem;
  border-block-end: 1px solid var(--rule);
}

.link-preview-text { min-width: 0; }
.link-preview-title { font-weight: 600; overflow-wrap: anywhere; }
.link-preview-source {
  display: block;
  color: var(--text-muted);
  font-size: var(--size-small);
  margin-block: 0.25rem;
}
.link-preview p { margin: 0.5rem 0 0; }
.link-preview-image {
  width: clamp(4rem, 18vw, 8rem);
  aspect-ratio: 4 / 3;
  object-fit: cover;
  flex-shrink: 0;
  border-radius: 0.25rem;
}

main {
  /* `dvh`, not `vh`, for the same reason the book's sticky panel uses
     it: on mobile `vh` is the largest viewport, so a short page reserves
     more height than the screen actually has. */
  min-block-size: 60dvh;
}

/* Inside the band, prose narrows to the measure. `.book` is the
   exception because it is not prose — it is a sidebar beside prose, and
   the prose half caps itself. */
main > :not(.book) {
  max-inline-size: var(--measure);
  margin-inline: auto;
}

h1, h2, h3 {
  /* Headings break into even lines instead of leaving one orphaned
     word on the last one. */
  text-wrap: balance;
  line-height: 1.25;
}

h1 {
  font-size: var(--size-h1);
  margin-block-end: 1.25rem;
  letter-spacing: -0.01em;
}

h2 {
  font-size: var(--size-h2);
  margin-block: 2rem 0.625rem;
}

h3 {
  font-size: var(--size-h3);
  margin-block: 1.5rem 0.5rem;
}

/* Anything an anchor can land on clears the top of the viewport. */
:target {
  scroll-margin-block-start: 1.5rem;
}

p {
  margin-block-end: 1rem;
  /* No single-word last lines, and no rivers in a narrow column. */
  text-wrap: pretty;
}

a {
  color: var(--accent);
}

a:hover {
  color: var(--accent-strong);
}

/* Text that pointed at a page this site does not publish. It keeps the
   words and loses the link — see `unlink` in templates.swift. */
.unlinked {
  color: var(--text-muted);
}

article {
  margin-block-end: 2.5rem;
}

/* Nothing animates for a reader who asked for that, including the
   smooth scrolling above — which is a genuine trigger for some people
   and not a decoration worth defending. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

.tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-block-end: 1.25rem;
  padding-inline-start: 0;
}

.tags li {
  margin-block-end: 0;
}

.tags li a {
  display: inline-block;
  background: var(--chip);
  padding: 0.125rem 0.625rem;
  border-radius: 999px;
  font-size: var(--size-small);
  color: var(--text-muted);
  text-decoration: none;
}

.tags li a:hover {
  background: var(--border);
}

.article-card {
  padding-block: 1.25rem;
  border-block-end: 1px solid var(--rule);
}

/* The last card in a list is followed by the footer's own rule, and two
   hairlines a few pixels apart read as a mistake. */
.article-card:last-of-type {
  border-block-end: 0;
}

.article-card h2 {
  margin: 0 0 0.5rem;
  font-size: var(--size-h2);
}

.article-card p {
  color: var(--text-muted);
  margin: 0;
}

footer {
  margin-block-start: 3.75rem;
  padding-block: 1.25rem;
  /* Clear of a phone's home indicator. */
  padding-block-end: max(1.25rem, env(safe-area-inset-bottom));
  border-block-start: 1px solid var(--rule);
  color: var(--text-faint);
  font-size: var(--size-small);
}

ol, ul {
  margin-block-end: 1rem;
  padding-inline-start: 1.5rem;
}

li {
  margin-block-end: 0.25rem;
}

code {
  background: var(--surface-alt);
  padding: 0.125rem 0.375rem;
  border-radius: var(--radius-sm);
  font-size: 0.9em;
  /* A long identifier in prose breaks rather than pushing the column
     wider than the page. */
  overflow-wrap: break-word;
}

pre {
  background: var(--surface-alt);
  padding: 1rem;
  border-radius: var(--radius);
  /* Scrolls inside its own box. The `overscroll-behavior` stops a
     horizontal flick inside a code block from turning into a back
     gesture once it reaches the end. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  margin-block-end: 1rem;
  tab-size: 2;
}

pre code {
  background: none;
  padding: 0;
  /* Code keeps its own line breaks — the wrapping above is for prose. */
  overflow-wrap: normal;
}

/* -------------------------------------------------------------------- search */

.search {
  position: relative;
  display: grid;
  /* Shrinkable, with a floor. `flex: 0 0 auto` made the field the one
     rigid thing in a flexible row, so between the tabs wrapping and the
     row wrapping there was a band of widths where the three items
     simply collided. It gives ground now, and stops at a width a query
     is still readable in. */
  flex: 0 1 200px;
  min-inline-size: 9rem;
}

.search-input {
  inline-size: 100%;
  padding: 6px 46px 6px 12px;
  font: inherit;
  font-size: 0.9rem;
  color: var(--text);
  background: var(--field);
  border: 1px solid var(--border);
  border-radius: 16px;
  appearance: none;
}

/* `outline: none` used to live here with a border colour standing in
   for it — invisible against several palettes, and gone entirely in
   forced-colours mode. The global `:focus-visible` ring applies here
   like everywhere else; this only adds the field's own emphasis. */
.search-input:focus-visible {
  border-color: var(--accent);
  background: var(--surface);
}

/* The hint shares the field's grid cell rather than being positioned
   against `.search`, which is not always a containing block — see the
   anchored panel below. */
.search-input,
.search-hint {
  grid-area: 1 / 1;
}

.search-hint {
  justify-self: end;
  align-self: center;
  margin-inline-end: 12px;
  font-size: 0.75rem;
  color: var(--text-faint);
  pointer-events: none;
}

.search-input:focus + .search-hint {
  display: none;
}

.search-results {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 10;
  width: 420px;
  max-inline-size: calc(100vw - 2 * var(--gutter));
  max-height: min(60vh, 460px);
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px var(--shadow);
}

.search-results[hidden] {
  display: none;
}

/* Hanging the panel off the field's right edge is right while the field
   ends the header row. When the tabs wrap, the field starts the second
   row instead, and a right-hung panel runs off the left of the screen.
   Which row the field lands on depends on how many tabs a site has, so
   no breakpoint can say; anchor positioning measures it. The panel is
   positioned against the viewport and tethered to the field, and flips
   to hang from the field's left edge whenever the right would not fit.
   Without anchor support it keeps the right-hung position above. Phones
   are left alone: there the field already spans its own row. */
@media (min-width: 700px) {
  @supports (position-try-fallbacks: flip-inline) {
    .search {
      position: static;
      anchor-name: --search;
    }

    .search-results {
      position-anchor: --search;
      top: auto;
      right: auto;
      position-area: bottom span-left;
      position-try-fallbacks: flip-inline;
      margin-block-start: 6px;
    }
  }
}

.search-result {
  display: block;
  padding: 10px 14px;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: var(--text);
}

.search-result:last-child {
  border-bottom: 0;
}

.search-result.active,
.search-result:hover {
  background: var(--accent-wash);
}

.search-result-title {
  display: block;
  font-weight: 600;
  color: var(--accent);
}

.search-result-section {
  display: block;
  font-size: 0.75rem;
  color: var(--text-faint);
}

.search-result-snippet {
  display: block;
  margin-top: 2px;
  font-size: 0.85rem;
  color: var(--text-muted);
}

.search-result mark {
  background: var(--highlight);
  color: inherit;
}

.search-empty {
  margin: 0;
  padding: 12px 14px;
  color: var(--text-faint);
  font-size: 0.9rem;
}

@media (max-width: 699px) {
  header nav {
    flex-wrap: wrap;
    gap: 10px;
  }

  /* Below this the row is two lines: title and tabs on the first, the
     field spanning the second. A search box squeezed into a phone's
     header row is a search box nobody types in. */
  .search {
    flex: 1 0 100%;
  }

  .search-results {
    width: 100%;
    max-inline-size: none;
  }
}

/* ---------------------------------------------------------------- guide book */

/* A book's band is exactly what its two columns need — contents, gap,
   and a full measure of prose. Derived rather than picked, so that
   changing the measure moves the header, the footer and the sidebar
   together instead of leaving one of them a stale number. */
body.guidebook {
  /* The gutters are in the sum because `box-sizing: border-box` puts
     the band's own padding inside `max-inline-size`. Left out, the
     prose column came up exactly two gutters short of a measure. */
  --shell: calc(var(--toc) + var(--gap) + var(--measure) + 2 * var(--gutter));
}

/* Grid, not flex, and the difference is what this layout got wrong.
   As a flex row the hamburger button was a *third column*: on a phone
   the table of contents went `position: fixed` and left the flow, but
   the button did not, so it sat in its own track with a 40px gap beside
   it and the prose started roughly halfway across the screen.

   Named areas make the two arrangements say what they are. Narrow is
   one column with the button above the text; wide puts the contents
   beside it and drops the button entirely. Neither is expressed as a
   width the other has to work around. */
.book {
  display: grid;
  grid-template-areas:
    "toggle"
    "page";
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gap);
  align-items: start;
}

.book-page {
  grid-area: page;
  /* `minmax(0, …)` above and this together: a grid item's default
     minimum size is its content, so one wide `<pre>` would otherwise
     stretch the column and push the page sideways. */
  min-inline-size: 0;
  max-inline-size: var(--measure);
}

.toc-toggle {
  grid-area: toggle;
  /* The button is only ever a narrow-screen affordance, and
     `justify-self` keeps it from stretching across the column. */
  justify-self: start;
}

.toc-heading {
  font-weight: 700;
  padding-block-end: 0.5rem;
  margin-block-end: 0.5rem;
  border-block-end: 1px solid var(--rule);
  text-wrap: balance;
}

.toc-heading a {
  color: var(--text);
  text-decoration: none;
}

.toc-list {
  list-style: none;
  margin: 0;
  padding-inline-start: 0;
  font-size: 0.92rem;
}

.toc-list .toc-list {
  padding-inline-start: 0.875rem;
}

.toc-list li {
  margin-block-end: 0.125rem;
}

.toc-link,
.toc-link-empty {
  display: block;
  /* Block, and padded, so the whole row is the target rather than the
     glyphs. On a phone the drawer is reached by thumb, and a two-pixel
     text hit area is the difference between a link and a lottery. */
  padding-block: 0.25rem;
  color: var(--text-muted);
  text-decoration: none;
  text-wrap: pretty;
}

.toc-link:hover {
  color: var(--accent);
}

.toc-link.current {
  color: var(--accent);
  font-weight: 700;
}

/* A `<summary>` puts its disclosure marker and its content on one row,
   and the block-level `.toc-link` below would push the label onto the
   line under the triangle — one wasted line per group, and a marker
   that stops reading as belonging to the words beside it.
   
   Making the link inline again is the fix, and it has to be done this
   way round: `display: flex` on the summary also lines them up, but
   Chrome drops the disclosure marker entirely for any summary that is
   not `display: list-item`, so the fix costs the one affordance that
   says a group opens. */
.toc-summary {
  cursor: pointer;
  color: var(--text);
  padding-block: 0.125rem;
}

.toc-summary .toc-link {
  display: inline;
  padding-block: 0;
  color: var(--text);
  font-weight: 600;
}

/* Safari still needs the legacy pseudo-element to restyle the marker,
   and `list-style` covers everyone else. */
.toc-summary::marker,
.toc-summary::-webkit-details-marker {
  color: var(--text-faint);
  font-size: 0.75em;
}

.toc-summary .toc-link.current {
  color: var(--accent);
}

/* The hamburger button only exists on narrow screens; see the media
   queries below. */
.toc-toggle {
  display: none;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.875rem;
  font: inherit;
  font-size: 0.9rem;
  color: var(--text);
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.toc-toggle:hover {
  background: var(--chip-hover);
}

.toc-toggle-icon {
  font-size: 1.1rem;
  line-height: 1;
}

/* Wide: the table of contents is a plain sticky sidebar. The popover attribute stays on the
   element, so these rules have to undo the browser's default popover presentation. */
@media (min-width: 900px) {
  .book {
    grid-template-areas: "toc page";
    grid-template-columns: var(--toc) minmax(0, 1fr);
  }

  .toc[popover] {
    display: block;
    grid-area: toc;
    /* The UA stylesheet gives `[popover]` `width: fit-content`, and it
       still applies to an element whose popover presentation is
       otherwise undone here. Without this the contents shrink-wrap to
       their longest title inside the track — which looks fine until
       a title is long enough to wrap, and then wraps in a column
       narrower than the space beside it. */
    inline-size: 100%;
    position: sticky;
    /* `dvh`, not `vh`: on a phone in landscape and on desktop browsers
       that hide chrome on scroll, `vh` is the *largest* viewport, so a
       full-height sticky panel is taller than what is actually on
       screen and its last entries sit below the fold with no way to
       reach them. */
    inset-block-start: 1.25rem;
    inset: auto;
    max-block-size: calc(100dvh - 2.5rem);
    overflow-y: auto;
    /* A panel that has reached its end shouldn't hand the scroll to the
       page behind it mid-gesture. */
    overscroll-behavior: contain;
    /* Undo the browser's default popover presentation — the attribute
       stays on the element so the same markup can become a drawer at
       the breakpoint below. */
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
  }
}

/* Narrow: the same markup becomes a popover panel opened by the hamburger button. */
@media (max-width: 899px) {
  .toc-toggle {
    display: inline-flex;
  }

  .toc[popover] {
    position: fixed;
    inset: 0 auto 0 0;
    inline-size: min(20rem, 85vw);
    max-inline-size: none;
    max-block-size: none;
    block-size: 100dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
    margin: 0;
    padding: var(--gutter);
    border: 0;
    border-inline-end: 1px solid var(--border);
    background: var(--surface);
    color: inherit;
    /* Room for a thumb below the last entry on a phone with a home
       indicator or a bottom bar. */
    padding-block-end: max(var(--gutter), env(safe-area-inset-bottom));
  }

  .toc[popover]::backdrop {
    background: var(--scrim);
  }
}

/* -------------------------------------------------- previous / next navigation */

/* Two cards that share the row: previous on the left, next on the
   right, and either one alone still sits on its own side. Grid with
   two equal tracks does that without the `margin-left: auto` trick the
   flex version needed — and collapses to one column on a phone, where
   two half-width targets are too small to hit. */
.page-nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  gap: 1rem;
  border-block-start: 1px solid var(--rule);
  padding-block-start: 1.25rem;
  margin-block-start: 2.5rem;
}

.page-nav-top {
  border-top: 0;
  border-bottom: 1px solid var(--rule);
  padding-block-start: 0;
  padding-block-end: 1.25rem;
  margin-block-start: 0;
  margin-block-end: 1.875rem;
}

.page-nav-link {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  text-decoration: none;
  transition: border-color 120ms ease;
}

.page-nav-link:hover {
  border-color: var(--border-strong);
}

/* `grid-column: 2` rather than `margin-left: auto`: when a page has a
   next and no previous — the first page of a book — the auto margin
   left it in the first track, so "Next" sat on the left where the
   reader's eye looks for "Previous". Naming the column puts it on the
   right whether or not it has a sibling. */
.page-nav-next {
  text-align: end;
  grid-column: 2;
}

/* …except when the row has collapsed to one column, where there is no
   second track to name and the text should read from the start. */
@media (max-width: 34rem) {
  .page-nav-next {
    grid-column: 1;
    text-align: start;
  }
}

.page-nav-label {
  font-size: var(--size-tiny);
  color: var(--text-faint);
}

.page-nav-title {
  color: var(--accent);
  text-wrap: balance;
}

/* Tables. Prose markdown grows one the moment a page has to compare
   two things, and an unstyled table runs its columns together into
   something that reads as a paragraph with odd line breaks.

   `display: block` on the wrapper is what keeps a wide table from
   widening the whole page: it scrolls inside its own box instead of
   pushing the body sideways, which on a phone is the difference
   between a scrollable table and an unreadable site. */
.page table,
.book table {
  display: block;
  overflow-x: auto;
  width: 100%;
  border-collapse: collapse;
  margin: 1.5rem 0;
  font-size: 0.95rem;
}

.page table th,
.book table th {
  text-align: left;
  font-weight: 600;
  color: var(--text-muted);
  border-bottom: 2px solid var(--border);
  padding: 0.5rem 0.9rem 0.5rem 0;
  white-space: nowrap;
}

.page table td,
.book table td {
  border-bottom: 1px solid var(--rule);
  padding: 0.5rem 0.9rem 0.5rem 0;
  vertical-align: top;
}

.page table tr:last-child td,
.book table tr:last-child td {
  border-bottom: none;
}

/* A cell is usually a short label or an identifier, and wrapping the
   code style from prose makes those columns twice as tall. */
.page table code,
.book table code {
  white-space: nowrap;
}