@font-face {
  /* Fraunces, subset from the upstream variable font: Latin ranges only, SOFT
     and WONK pinned to the values used below, opsz kept so headings still get
     their optical size. 118 -> 62 KB. Rebuild: docs/reference.md#the-display-font. */
  font-family: "Fraunces";
  /* relative to this stylesheet, so it resolves under any -base-path */
  src: url("fonts/fraunces.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* The browser's own [hidden] rule is display:none, and any author rule with a
   display beats it: .card sets display:flex, so search marked non-matching
   cards hidden and left them on screen while the live region announced
   "1 result". The same slip left the theme toggle and the dismiss button
   clickable and inert without JavaScript. !important restores the invariant so
   a later rule cannot take it back; every script that reveals a control clears
   the attribute (theme.js, about.js, search.js). */
[hidden] {
  display: none !important;
}

:root {
  /* Mineral palette: cool weathered stone, not warm cream. */
  color-scheme: light dark;
  --bg: light-dark(#eef0ea, #14181a);
  --card: light-dark(#fbfbf9, #1b2123);
  --fg: light-dark(#1b2220, #e6e9e5);
  --muted: light-dark(#5c6461, #98a19d);
  --faint: light-dark(#e8e7df, #212829);
  /* Decoration: card outlines, rules, the trail's spine. WCAG 1.4.11 does not
     reach it. At 1.21:1 against --bg in light and 1.34:1 in dark it was a
     hairline that disappeared on a cheap screen in daylight; around 2:1 is the
     floor. Measured: light 1.84:1 on --bg and 2.04:1 on --card, dark 2.02:1
     and 1.84:1. */
  --border: light-dark(#b4b3a8, #434c4a);
  /* The boundary that is the control, where nothing else says where to click or
     type and 1.4.11 does ask 3:1. Separate from --border so the decoration
     above can be chosen for looks and this one cannot. Measured against both
     neighbours: light 3.31:1 on --bg and 3.67:1 on --card, dark 3.63:1 and
     3.31:1. */
  --ui-border: light-dark(#7f8482, #6b7270);
  --accent: #247b7b;                                       /* set inline from theme.accent */
  --accent-ink: light-dark(color-mix(in oklab, var(--accent) 85%, #000), color-mix(in oklab, var(--accent) 72%, #fff));
  --up: light-dark(#2e8b57, #4cb47c);
  --down: light-dark(#c0392b, #e0685b);
  /* Two more states, for monitors that report more than a bool. WCAG 1.4.11
     asks 3:1 of a graphic that carries meaning. Measured against the pill's own
     fill, the only neighbour a dot has: degraded 4.29:1 light and 5.95:1 dark,
     maintenance 4.99:1 and 5.85:1. */
  --degraded: light-dark(#a86a00, #cf9020);
  --maintenance: light-dark(#2f6fb5, #5aa0e0);
  /* A status token paints a dot, a graphic that needs 3:1. Used raw as a word
     it fails: green measured 4.10:1 and amber 4.29:1 against a card. Each is
     pushed until it carries text: beta 8.20:1, deprecated 8.07:1, soon 7.69:1. */
  --ink-beta: light-dark(color-mix(in oklab, var(--up) 72%, #000), color-mix(in oklab, var(--up) 82%, #fff));
  --ink-deprecated: light-dark(color-mix(in oklab, var(--degraded) 74%, #000), color-mix(in oklab, var(--degraded) 88%, #fff));
  --ink-soon: light-dark(color-mix(in oklab, var(--maintenance) 78%, #000), color-mix(in oklab, var(--maintenance) 85%, #fff));

  --shadow: 0 1px 2px light-dark(rgba(20, 30, 28, .04), rgba(0, 0, 0, .2)), 0 10px 26px -14px light-dark(rgba(20, 30, 28, .14), rgba(0, 0, 0, .5));
  --shadow-lift: 0 2px 4px light-dark(rgba(20, 30, 28, .05), rgba(0, 0, 0, .25)), 0 18px 42px -16px light-dark(rgba(20, 30, 28, .24), rgba(0, 0, 0, .62));

  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: "Fraunces", ui-serif, Georgia, "Times New Roman", serif;

  --radius: .9rem;
  --maxw: 66rem;
  /* the sticky header on narrow screens; .skip has to paint one above it */
  --z-header: 40;
  /* How far a waypoint diamond drops from the centre of its line box, as a
     fraction of the label's font size.

     A line box carries descender space no lowercase letter reaches, so a mark
     centred in it reads high beside a word that is mostly lowercase. Dropping
     it all the way onto the middle of the lowercase then reads low, because
     the capital and the ascenders carry mass above that band with nothing
     under it to answer. This lands halfway between the two centres.

     Measured against a baseline the layout reports, not one computed from the
     font's own ascent: those differ by about 0.45px here, which is most of
     what is being corrected. A zero-height inline-block sits on the baseline
     and 1ex is the x-height, so both come from the browser. Read that way the
     rail's sans and the heading's Fraunces want the same fraction, which is
     why one number serves both. */
  --mark-drop: .0356;
  --way-font: 1.5rem;
  /* The rail's diamond, named because the spine has to be centred on it and a
     second literal would drift from the first. */
  --toc-mark: .42rem;
}

:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: inherit; }

.wrap {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: 1.5rem;
}

/* ---- header ---- */

header { padding-block: 1.4rem; }
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 40;
  font-weight: 560;
  font-size: 1.3rem;
  letter-spacing: -.01em;
  text-decoration: none;
}
.brand img { height: 1.7rem; width: auto; }
.foot-mark svg { display: block; width: 100%; height: 100%; }

.menu {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem 1.5rem;
  margin-top: .8rem;
  padding-block: .5rem;
  border-top: 1px solid var(--border);
}
.menu-link {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-size: .88rem;
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  transition: color .16s;
}
.menu-link:hover { color: var(--fg); }
.menu-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.menu-link svg { width: .95rem; height: .95rem; opacity: .85; flex: none; }
.menu-link img { width: 1rem; height: 1rem; flex: none; }

/* The header links live in a <details>: transparent on desktop so they flow
   inline, a burger and a dropdown in the mobile block. */
.nav { display: contents; }
.nav-burger { display: none; }
/* A closed <details> hides its content: newer engines through
   content-visibility on ::details-content, older ones through display:none.
   Both are undone here; the mobile block shows and hides via .nav-links. */
.nav::details-content { content-visibility: visible; display: contents; }
.nav-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem 1.5rem;
  min-width: 0;
}

.topbar-tools { display: flex; align-items: center; gap: .5rem; }
.theme-toggle {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: .5rem;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color .16s;
}
.theme-toggle:hover { color: var(--fg); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.theme-toggle svg { width: 1.05rem; height: 1.05rem; display: none; }
/* offer the other theme: moon while light, sun while dark */
.theme-toggle .moon { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .moon { display: none; }
  :root:not([data-theme="light"]) .theme-toggle .sun { display: block; }
}
:root[data-theme="dark"] .theme-toggle .moon { display: none; }
:root[data-theme="dark"] .theme-toggle .sun { display: block; }

.langs { display: flex; gap: .2rem; }
.langs a,
.langs [aria-current] {
  padding: .3rem .6rem;
  border: 1px solid transparent;
  border-radius: .5rem;
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: .06em;
  text-decoration: none;
  color: var(--muted);
}
.langs [aria-current] {
  color: var(--fg);
  background: var(--card);
  border-color: var(--border);
}
.langs a:hover { color: var(--fg); }
.langs a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Above three locales the switcher folds into a menu; <details> keeps it
   working without JavaScript. */
.langs-menu { position: relative; display: block; }
.langs-menu summary {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  list-style: none;
  cursor: pointer;
  padding: .3rem .6rem;
  border: 1px solid var(--border);
  border-radius: .5rem;
  background: var(--card);
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: .06em;
  color: var(--fg);
}
.langs-menu summary::-webkit-details-marker { display: none; }
.langs-menu summary svg { width: .85rem; height: .85rem; color: var(--muted); }
.langs-menu[open] summary svg { transform: rotate(180deg); }
.langs-menu summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.langs-menu nav {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + .4rem);
  display: flex;
  flex-direction: column;
  min-width: 5.5rem;
  padding: .3rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: .7rem;
  box-shadow: var(--shadow-lift);
  z-index: 10;
}
.langs-menu nav a,
.langs-menu nav [aria-current] {
  padding: .35rem .6rem;
  border-radius: .45rem;
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: .06em;
  text-decoration: none;
  color: var(--muted);
}
.langs-menu nav [aria-current] { color: var(--fg); background: var(--faint); }
.langs-menu nav a:hover { color: var(--fg); background: var(--faint); }
.langs-menu nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- welcome note ---- */

[data-noabout] .about { display: none; }
.about {
  position: relative;
  margin: 2.2rem 0 2.4rem;
  /* Logical, not physical: the close button sits at inset-inline-end, so under
     RTL a physical end padding stayed put while the button moved across and
     landed on the text. */
  padding-block: 1.3rem;
  padding-inline: 1.5rem 3.4rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.about p { line-height: 1.7; margin: 0 0 .85rem; }
.about p:last-of-type { margin-bottom: 0; }
/* The same fold as the host flag: a glyph at rest, the word unfurled beside it
   on hover or focus. Swapping display between the two cannot be animated, since
   display does not interpolate, and it took the cross away at the instant the
   pointer landed on it. Clipping a nowrap row keeps both parts on screen and
   gives the width something to travel. */
.about-x {
  position: absolute;
  top: .8rem;
  inset-inline-end: .8rem;
  display: inline-flex;
  align-items: center;
  gap: .32rem;
  /* Folded it is a disc, not a pill: as wide as it is tall. The border, the
     glyph and the two paddings have to add up to exactly that, border pixel
     included, or the box goes oval and the cross sits off centre. */
  --x-disc: 1.9rem;
  --x-glyph: .8rem;
  height: var(--x-disc);
  max-width: var(--x-disc);
  overflow: hidden;
  padding: 0 calc((var(--x-disc) - var(--x-glyph)) / 2 - 1px);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--faint);
  color: var(--muted);
  font: inherit;
  font-size: .78rem;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: max-width .3s cubic-bezier(.2, .7, .3, 1), color .16s, border-color .16s;
}
/* flex: none, or the glyph is what gives way when the box is clipped */
.about-x svg { width: var(--x-glyph); height: var(--x-glyph); flex: none; }
.about-x span { opacity: 0; transition: opacity .18s; }
.about-x:hover, .about-x:focus-visible {
  max-width: 14rem;
  color: var(--fg);
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
}
.about-x:hover span, .about-x:focus-visible span { opacity: 1; }
.about-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* No (hover: none) rule here, unlike the flag. The flag sits in the card foot,
   in flow, so an unfurled label costs nothing. This button is absolute over the
   note, which reserves 3.4rem of end padding for it folded: unfurled on a phone
   it would lay its label across the first line of the welcome text. */

/* Width on the wrapper: as a flex item the box would otherwise size to its own
   max-content and stop short of the inline end. */
.search {
  margin-inline-start: auto;
  width: min(14.5rem, 100%);
  transition: width .2s ease;
}
.search:focus-within { width: min(19rem, 100%); }
/* The bar renders on every page for layout parity. Only the home page has a
   script to wake it; elsewhere it stays disabled and decorative. */
.search-box:has(input:disabled) {
  background: var(--faint);
  box-shadow: none;
  opacity: .55;
  cursor: not-allowed;
}
.search-box input:disabled { cursor: not-allowed; }
.search-box {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding: .05rem .3rem .05rem .85rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: var(--shadow);
  transition: border-color .15s, box-shadow .15s;
}
/* An input's outline is the whole of what says where to type, so WCAG 1.4.11
   wants 3:1 there. It exempts inactive components, which is what the bar is on
   every page but the home page, so the strong border goes where the control is
   awake and the quiet one stays where it is asleep.
   :where() holds this at .search-box's own specificity so :focus-within below
   still gets to paint the border accent. */
.search-box:where(:has(input:enabled)) { border-color: var(--ui-border); }
@media (max-width: 44rem) {
  .search, .search:focus-within { flex: 1 1 100%; margin-inline-start: 0; width: 100%; }
}
.search-box:focus-within {
  border-color: var(--accent);
  box-shadow: var(--shadow-lift);
}
.search-box svg { width: 1rem; height: 1rem; color: var(--muted); flex: none; }
.search-box input {
  flex: 1;
  min-width: 0;
  padding: .45rem .2rem;
  border: 0;
  background: none;
  font: inherit;
  font-size: .88rem;
  color: inherit;
}
.search-box input:focus { outline: none; }
.search-kbd {
  flex: none;
  margin-inline-end: .5rem;
  padding: .1rem .45rem;
  font-family: inherit;
  font-size: .72rem;
  font-weight: 500;
  color: var(--muted);
  background: var(--faint);
  border: 1px solid var(--border);
  border-radius: .45rem;
}
@media (hover: none) { .search-kbd { display: none; } }

/* ---- category waypoints ---- */

.way {
  display: flex;
  align-items: center;
  gap: .7rem;
  margin: 2.6rem 0 1.1rem;
  /* the containing block for the link glyph, which hangs off the start edge */
  position: relative;
}
.way-mark {
  width: .5rem;
  height: .5rem;
  flex: none;
  border-radius: 2px;
  background: var(--accent);
  transform: rotate(45deg);
  position: relative;
  top: calc(var(--way-font) * var(--mark-drop));
}
.way-name {
  margin: 0;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 60;
  font-weight: 540;
  font-size: var(--way-font);
  letter-spacing: -.012em;
}
/* The whole heading is the anchor, the way its entry in the rail is. It takes
   neither colour nor underline: a heading that changed weight or colour on
   hover would read as a different heading. The pointer says it is a target,
   and the focus ring says so to a keyboard. */
.way-name a {
  color: inherit;
  text-decoration: none;
}
/* The affordance a markdown renderer puts beside a heading. It sits in the
   margin rather than in the row, so nothing shifts when it appears, and it is
   inside the anchor, so clicking it does what it says. aria-hidden because the
   heading beside it is that same link and would otherwise be announced twice. */
.way-link {
  position: absolute;
  inset-inline-end: 100%;
  margin-inline-end: .55rem;
  top: 50%;
  transform: translateY(-50%);
  width: 1.05rem;
  height: 1.05rem;
  /* --muted held back to .65 rather than --border at full strength. Tried at
     the rule's weight first, which is tidier on paper since the rule closes
     the same row, and it read as a smudge at real size: a glyph is line work
     with gaps in it, not a solid 1px bar, so the same ink does not carry the
     same distance. */
  color: var(--muted);
  opacity: 0;
  transition: opacity .14s;
}
/* Only where the margin can hold it. Below this the content sits against the
   wrap's own padding and there is nothing to hang it in: measured, 32.5px of
   margin at 68rem against the 26px the glyph and its gap need, and 24px at
   64rem. And only where there is a pointer, since a tap has no hover to
   reveal it with. */
@media (hover: hover) and (min-width: 69rem) {
  .way-name a:hover .way-link,
  .way-name a:focus-visible .way-link {
    opacity: .65;
  }
}
.way-name a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 3px;
}
.way-rule {
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ---- category trail ---- */

/* A fixed rail beside the content: one diamond per category on a vertical line.
   Shown only where the margin can hold it. */
.toc { display: none; }
@media (min-width: 88rem) {
  .toc {
    display: block;
    position: fixed;
    top: 8.6rem;
    inset-inline-start: calc(50% + var(--maxw) / 2 + 1.6rem);
    max-width: 10rem;
  }
}
.toc ul {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
}
.toc ul::before {
  content: "";
  position: absolute;
  /* Centred on the diamonds rather than set near them: half the mark, less
     half the line's own width. Written as .2rem it drew 0.34px to their right,
     which is invisible until something moves and then is not. */
  inset-inline-start: calc(var(--toc-mark) / 2 - .5px);
  top: .8rem;
  bottom: .8rem;
  width: 1px;
  background: var(--border);
}
.toc a {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .28rem 0;
  font-size: .82rem;
  color: var(--muted);
  text-decoration: none;
  transition: color .16s;
}
.toc-mark {
  width: var(--toc-mark);
  height: var(--toc-mark);
  flex: none;
  border-radius: 1.5px;
  background: var(--border);
  transform: rotate(45deg);
  transition: background .16s;
  position: relative;
  /* em here: the rail's mark does inherit its label's size. */
  top: calc(1em * var(--mark-drop));
}
.toc-name {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.toc a:hover { color: inherit; }
.toc a:hover .toc-mark { background: var(--muted); }
.toc a[aria-current] { color: inherit; font-weight: 600; }
.toc a[aria-current] .toc-mark { background: var(--accent); }
.toc a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

/* Below the rail the trail is a row of chips that scrolls sideways rather than
   wrapping: one line at any category count, on a 320px phone as on a laptop
   window. It replaces two controls that used to split at seven categories, a
   wrapping row and a jump-to select, and it closes the band between 44rem and
   88rem where a reader had no category navigation at all.

   toc.js keeps the current chip in view. Without it the row is still a list
   you can swipe, which the select it replaces was not: that one did nothing
   without JavaScript, which is why it had to be swapped in behind data-js. */
@media (max-width: 87.99rem) {
  .toc {
    display: block;
    margin: 1.5rem 0 .3rem;
  }
  .toc ul {
    display: flex;
    flex-wrap: nowrap;
    gap: .5rem;
    overflow-x: auto;
    /* the row is the scroller, so its own focus ring must not be clipped */
    padding-block-end: 2px;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
  }
  .toc ul::-webkit-scrollbar { display: none; }
  /* The side with chips behind it fades, so the row cannot be mistaken for a
     complete list. toc.js sets the attribute; without it the row still scrolls,
     it just does not advertise it. */
  .toc ul[data-more="right"] {
    mask-image: linear-gradient(to right, #000 calc(100% - 2.2rem), transparent);
  }
  .toc ul[data-more="left"] {
    mask-image: linear-gradient(to right, transparent, #000 2.2rem);
  }
  .toc ul[data-more="left right"] {
    mask-image: linear-gradient(to right, transparent, #000 2.2rem, #000 calc(100% - 2.2rem), transparent);
  }
  .toc ul::before { display: none; }
  .toc li { flex: none; scroll-snap-align: center; }
  .toc a {
    min-height: 2.5rem;
    padding: 0 .85rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--card);
    font-size: .85rem;
    white-space: nowrap;
  }
  .toc-mark { display: none; }
  /* No current one. A chip row is a set of buttons that take you somewhere,
     not a report of where you are: the rail above 88rem is the thing that
     tracks the reader, and it has the room to say so without a chip having to
     light up under their thumb. aria-current is still set, since it costs
     nothing and a screen reader can use it. */
  .toc a[aria-current] {
    color: var(--muted);
    font-weight: inherit;
    border-color: var(--border);
    background: var(--card);
  }
}

/* On the heading and not on .cat: every #cat- link, the rail's and the jump
   select's and now the heading's own, targets the h2, so the section's margin
   was never the one the browser read. 2.6rem is the gap above a heading, and
   anything past it brings the previous section's last card into view; this
   keeps 9px of that gap in hand.

   One value, phones included. The 4.5rem a narrow screen used to carry was
   meant to clear the sticky header, but it sat on .cat and so never applied,
   and it cannot do that job anyway: the header is 132px, which is past the gap
   by a long way. nav.js hides it on the way down, which is the direction a
   trail link travels, so a jump downward lands clear of it. A jump upward
   brings the header back over the heading, and that is not new. */
.way-name { scroll-margin-top: 2rem; }
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
html { scrollbar-gutter: stable; }

/* ---- cards ---- */

.cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: 1rem;
}
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  transition: border-color .16s, transform .16s, box-shadow .16s;
}
.card:hover {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--border));
  transform: translateY(-3px);
  box-shadow: var(--shadow-lift);
}
/* The keyboard-selected match, the card Enter opens. The accent border keeps
   the selection legible between beacon pulses and when motion is reduced. */
.card.sel {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 55%, transparent), var(--shadow-lift);
}
@media (prefers-reduced-motion: no-preference) {
  .card.sel { animation: card-beacon 2.8s cubic-bezier(.15, .6, .3, 1) infinite; }
}
@keyframes card-beacon {
  from { box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 55%, transparent), var(--shadow-lift); }
  to { box-shadow: 0 0 0 10px color-mix(in oklab, var(--accent) 0%, transparent), var(--shadow-lift); }
}
.card-main {
  display: flex;
  gap: .95rem;
  flex: 1;
  padding: 1.1rem;
}

.tile {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  flex: none;
  border-radius: .7rem;
  background: var(--faint);
  color: color-mix(in oklab, var(--accent) 55%, var(--muted));
}
.tile img { width: 1.9rem; height: 1.9rem; object-fit: contain; }
.tile svg { width: 1.7rem; height: 1.7rem; }

.card-text { display: flex; flex-direction: column; gap: .2rem; min-width: 0; flex: 1; }
.card-foot {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: 0 .85rem .8rem;
}
.card-foot .status-pill { margin-inline-start: auto; }
/* status.js swaps what is inside the slot, so the slot has to be in the markup
   before there is anything to swap. display:contents keeps a pill inside it a
   direct flex item of the foot, so the margin above still pushes it to the
   end. */
.status-slot { display: contents; }
/* A foot holding neither a flag nor a pill would draw its bottom padding around
   nothing: a site with a status page, for a service it does not monitor. */
.card-foot:not(:has(.flag, .status-pill)) { display: none; }

/* Host flag: an icon at rest that unfurls its label on hover or focus. The
   label stays in the DOM (clipped, not removed) for screen readers; on touch
   and under reduced motion it shows expanded.
   As a span it stays under the stretched-link overlay, so a click on it opens
   the service like the rest of the card. As a link, which it becomes when
   hosting_flag names a target, it is raised above: a third target inside the
   card, only on the sites that ask for it. */
.flag {
  display: inline-flex;
  align-items: center;
  gap: .32rem;
  max-width: 2rem;
  overflow: hidden;
  padding: .18rem .44rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  color: var(--muted);
  font-size: .72rem;
  font-weight: 500;
  white-space: nowrap;
  transition: max-width .3s cubic-bezier(.2, .7, .3, 1), color .2s, border-color .2s;
}
/* overflow visible: the cloud glyph reaches the viewBox edge and its stroke
   would be clipped flat; the flag's own padding absorbs the spill. */
.flag svg { width: .9rem; height: .9rem; flex: none; overflow: visible; }
.flag-label { opacity: 0; padding-inline-end: .14rem; transition: opacity .18s; }
.flag-self { color: var(--accent-ink); border-color: color-mix(in oklab, var(--accent) 32%, var(--border)); }
a.flag { position: relative; z-index: 1; text-decoration: none; }
a.flag:hover { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--card); }
a.flag:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A link has to be reachable at rest, behind its own fold: the icon alone is
   what a thumb arrives on, and 2rem by the padding and the glyph is the floor. */
a.flag { min-height: 1.5rem; }
.card:hover .flag, .card:focus-within .flag { max-width: 14rem; }
.card:hover .flag-label, .card:focus-within .flag-label { opacity: 1; }
@media (hover: none) {
  .flag { max-width: 14rem; }
  .flag-label { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .flag { transition: none; max-width: 14rem; }
  .flag-label { transition: none; opacity: 1; }
  /* the dismiss button keeps the fold and loses only the travel, unlike the
     flag above: it is absolute over the note and has no room to unfurl */
  .about-x, .about-x span { transition: none; }
  header, .nav-links { transition: none; }
}
/* Where a service stands, declared and not measured, so it carries no dot: the
   dot in the foot is what says a machine looked. It hangs on the top border
   rather than taking a row inside the card, because the grid stretches a row to
   its tallest card and a row inside charged 27px to every card beside it,
   measured. Out here it lives in the gap between rows, and .card declares no
   overflow to open. */
.card-badge {
  position: absolute;
  z-index: 2;
  inset-block-start: 0;
  inset-inline-start: 1rem;
  transform: translateY(-50%);
  padding: .12rem .55rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  /* the card's own fill, or the border it sits on shows through */
  background: inherit;
  color: var(--muted);
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.card-badge-beta { color: var(--ink-beta); border-color: color-mix(in oklab, var(--up) 40%, var(--border)); }
.card-badge-new { color: var(--accent-ink); border-color: color-mix(in oklab, var(--accent) 40%, var(--border)); }
.card-badge-deprecated { color: var(--ink-deprecated); border-color: color-mix(in oklab, var(--degraded) 40%, var(--border)); }
.card-badge-soon { color: var(--ink-soon); border-color: color-mix(in oklab, var(--maintenance) 40%, var(--border)); }
/* .card-badge-retired needs no rule: --muted above is already its colour. */

/* The two disabling states. A film over the card rather than lighter text: the
   card, the icon and the two chips recede together.
   Nothing readable goes under the film. Measured: at 45% no text reaches 4.5:1,
   not even painted near black, because the veil lifts ink and background
   together. The words stay above it and the film sits behind them. */
.card-off {
  box-shadow: none;
  background: color-mix(in oklab, var(--card) 30%, var(--bg));
  /* not a link any more, so not a pointer and nothing to lift */
  cursor: default;
}
.card-off:hover { transform: none; border-color: var(--border); }
.card-off .card-name,
.card-off .card-desc { color: var(--muted); }
.card-off .tile { filter: grayscale(1); }
.card-off::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--bg) 45%, transparent);
}
.card-off .card-text { position: relative; z-index: 2; }
/* A full-strength chip on a receded card read as wrong, so the flag and the
   glyph lose their own fill and ring too. Both measure 5.39:1 after. */
.card-off .flag {
  position: relative;
  z-index: 2;
  background: transparent;
  border-color: color-mix(in oklab, var(--border) 55%, transparent);
  color: var(--muted);
}
/* z-index alone. .card-more is already absolutely positioned and adding
   position:relative overrides that: the glyph leaves the card through the
   inline start and adds a line to its height. A browser check holds it. */
.card-off .card-more {
  z-index: 2;
  background: transparent;
  border-color: color-mix(in oklab, var(--border) 55%, transparent);
  color: var(--muted);
}

.card-name {
  min-width: 0;
  font-weight: 600;
  letter-spacing: -.005em;
  color: inherit;
  text-decoration: none;
}
/* Stretched link: the whole card follows the service link. The pill and the
   detail glyph are raised above it and stay independently clickable. */
.card-name::after { content: ""; position: absolute; inset: 0; z-index: 0; }
.card:has(.card-name:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-desc { color: var(--muted); font-size: .95rem; }

/* The detail link: a glyph in the card's corner, raised above the stretched
   link like the status pill. Inline at the end of the description it measured
   90 by 16, under the 24 by 24 WCAG 2.2 asks of a target, and no padding fixes
   that without pushing the prose around. In the card foot it squeezed the host
   flag: on a 320px phone the flag is unfurled and "Self-hosted" came out as
   "Self-". The corner belongs to nothing else. */
.card-more {
  position: absolute;
  inset-block-start: .85rem;
  inset-inline-end: .9rem;
  z-index: 1;
  display: grid;
  place-items: center;
  /* Whole pixels on both, so the gap between them is a whole number too: 28
     minus 16 leaves 6 a side with nothing to round. A card grid lands on
     fractional track widths (325.33px on the demo), so each card sits at its
     own subpixel phase, and a fractional gap rounds the ring and the glyph to
     different sides. Measured: 1 device pixel of disagreement at .95rem, 0 at
     16px, on every card and every phase. */
  --ring: 28px;
  --ring-glyph: 16px;
  width: var(--ring);
  height: var(--ring);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  color: var(--muted);
  transition: color .16s, border-color .16s;
}
/* Hover fills rather than tints. The whole card is a link, so the pointer is
   already a hand here, and tinting the border moved it from 2.04:1 to 2.82:1
   against the card, invisible next to the card lifting at the same moment.
   --accent-ink rather than --accent: it is the accent already adjusted per
   theme to read against the card, so the fill measures 6.82:1 in light and
   5.47:1 in dark, and the card-coloured mark on it the same, whatever accent
   the operator chose. */
/* THE HOVER RULE for outlined controls, and it holds everywhere rather than
   per place: an outlined control fills with --accent-ink and takes --card for
   its text. 1.16.1 established the fill, since tinting a border alone reads as
   nothing when the pointer is already a hand; what was missing until now was
   that the same control did it inside a card and not outside, so the status
   pill changed behaviour between the home page and a detail page.

   The primary button is --accent rather than --accent-ink and is filled at
   rest, so it stays the loudest thing on any page it appears on. */
.card-more:hover { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--card); }
.card-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.more-glyph { width: var(--ring-glyph); height: var(--ring-glyph); }
/* Room for it, so a long name does not run underneath. */
.card:has(.card-more) .card-name { padding-inline-end: 2.2rem; }

/* ---- status dots ---- */

/* Status pill: a dot and a label, linked to the status page when there is one. */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .18rem .6rem .18rem .5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  font-size: .76rem;
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
}
/* Only a pill that leads somewhere answers to a pointer: a display-only one
   (status.linked: false) is not a control. */
a.status-pill:hover { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--card); }
/* The dot goes with the text rather than keeping its state colour: green on
   the filled accent measures 1.30:1 in light and 1.12:1 in dark, which is a
   smudge and not a dot. Nothing is lost, since the label beside it says
   Online or Offline in words. */
a.status-pill:hover .dot { background: var(--card); }
a.status-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card .status-pill { flex: none; }
/* Raised above the card's stretched link so a click reaches the status page. A
   display-only pill stays underneath and the click opens the service. */
.card a.status-pill { position: relative; z-index: 1; }

.status-pill .dot { width: .55rem; height: .55rem; flex: none; border-radius: 50%; }
.status-up .dot { background: var(--up); }
.status-down .dot { background: var(--down); }
.status-down { border-color: color-mix(in oklab, var(--down) 35%, var(--border)); }
.status-degraded .dot { background: var(--degraded); }
/* Blue against green is the pairing hardest to tell apart, and the dot is nine
   pixels across. The label carries the meaning without colour, which is what
   1.4.1 asks; the square is the redundancy for the glance. */
.status-maintenance .dot { background: var(--maintenance); border-radius: 1px; }
.status-unknown .dot { background: transparent; box-shadow: inset 0 0 0 1.6px var(--muted); }

@media (prefers-reduced-motion: no-preference) {
  .status-up .dot { animation: beacon 2.8s cubic-bezier(.15, .6, .3, 1) infinite; }
}
@keyframes beacon {
  from { box-shadow: 0 0 0 0 color-mix(in oklab, var(--up) 55%, transparent); }
  to { box-shadow: 0 0 0 5px color-mix(in oklab, var(--up) 0%, transparent); }
}

/* ---- detail page ---- */

.detail { max-width: 44rem; margin-inline: auto; padding-block: 1.4rem 1rem; }
/* The way back out of a detail page. As a bare text link it measured 51 by 23,
   under the 24 by 24 WCAG 2.2 asks of a target, and its arrow was a CSS
   ::before, which some screen readers read out and a stylesheet that fails to
   load takes with it. A chip now, with the arrow inline in the markup. */
.back {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .3rem .7rem .3rem .55rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  font-size: .82rem;
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  transition: color .16s, border-color .16s, background .16s;
}
.back-glyph { width: .85rem; height: .85rem; flex: none; }
/* The fill moves, like every other outlined control here. Tinting the text and
   the border alone was what 1.16.1 already found invisible on a card control:
   the pointer is a hand over any link, so only a fill says this one is a
   button. --faint darkens it against the page in light and lightens it in
   dark, one step either way. */
.back:hover { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--card); }
.back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Back, name and action in one grid, so the action can sit in two places
   without a second copy of it: beside the name where there is room, and up on
   the back link's row where there is not. Both the back link and the action
   span every column on their row and are justified to opposite ends, so
   neither of them decides how wide the tile's column is. */
.detail-top {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  column-gap: 1rem;
  row-gap: 1.1rem;
  margin-top: 1.6rem;
}
.detail-top .back { grid-area: 1 / 1 / 2 / -1; justify-self: start; }
.detail-top .tile { grid-area: 2 / 1; width: 3.6rem; height: 3.6rem; }
.detail-top .tile img { width: 2.3rem; height: 2.3rem; }
.detail-top .tile svg { width: 2rem; height: 2rem; }
.detail-top h1 { grid-area: 2 / 2; min-width: 0; }
.detail-top .btn { grid-area: 2 / 3; justify-self: end; margin: 0; }
.detail-top .detail-state-row { grid-area: 3 / 1 / 4 / -1; }
.detail-top h1 { flex: 1 1 auto; min-width: 0; }
/* No card here, so no badge: the state shares the pill's row under the title.
   On a line of its own it read as an orphan, and the pill leads as the heavier
   object. */
.detail-state-row { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; }
/* A rule between them: one claim was measured, the other declared. Only when
   the slot before it holds something, though. A disabling state has no pill,
   and a slot the monitor has not answered yet is empty, and either would leave
   a stroke hanging off nothing. :empty is live, so the rule arrives with the
   pill status.js swaps in. */
.detail-state-row .status-slot:not(:empty) + .detail-state {
  padding-inline-start: .8rem;
  border-inline-start: 1px solid var(--border);
}
.detail-state {
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.detail-state-beta { color: var(--ink-beta); }
.detail-state-new { color: var(--accent-ink); }
.detail-state-deprecated { color: var(--ink-deprecated); }
.detail-state-soon { color: var(--ink-soon); }
.detail-top h1 {
  margin: 0;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 96;
  font-weight: 470;
  font-size: clamp(1.9rem, 4.5vw, 2.7rem);
  letter-spacing: -.015em;
}
.detail .lead { font-size: 1.2rem; margin-top: 1.4rem; }
.detail p { max-width: 40rem; }
.shot { margin: 1.6rem 0 0; }
.shot img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.shot figcaption { margin-top: .55rem; font-size: .85rem; color: var(--muted); }
.btn {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  margin-top: 1.4rem;
  padding: .62rem 1.25rem;
  font-size: .95rem;
  background: var(--accent);
  border-radius: 999px;
  color: #fff;
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--shadow);
  transition: transform .15s, box-shadow .15s;
}
/* Sized in em so it follows the label, and mirrored under RTL like the back
   arrow: an arrow leaving its frame points the other way when the text does. */
.btn-glyph { width: 1.05em; height: 1.05em; flex: none; }
[dir="rtl"] .btn-glyph { transform: scaleX(-1); }

.btn:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
.btn:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

/* ---- leaving the site ----
   A native <dialog>, so ::backdrop, the focus trap and Escape are the browser's
   job. Nothing here positions it: a modal dialog is centred by the UA already,
   and reproducing that by hand puts it off screen on a short viewport. */
.leave {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--fg);
  padding: 1.6rem;
  max-width: min(34rem, calc(100vw - 2rem));
  box-shadow: var(--shadow-lift);
}
/* A modal dialog takes the interaction but not the scroll: under a wheel or a
   thumb the page still travels behind it. Keyed on the dialog's own open state
   rather than on a class leave.js would have to add and remove, so Escape, the
   backdrop and the stay button all undo it without anyone remembering to.
   scrollbar-gutter holds the width the scrollbar had, or the page jumps
   sideways by it as the bar goes. */
html:has(dialog[open]) {
  overflow: hidden;
  scrollbar-gutter: stable;
}
.leave::backdrop { background: light-dark(rgba(20, 30, 28, .38), rgba(0, 0, 0, .62)); }
.leave h2 { font-family: var(--font-display); font-size: 1.25rem; margin: 0 0 .6rem; }
.leave-body { margin: 0 0 1rem; color: var(--muted); }
/* The address, split in two because the origin says who you are about to talk
   to and a long path pushes it out of sight. anywhere, not break-all, so a
   hostname breaks at its dots rather than mid-label. */
.leave-url {
  margin: 0 0 1.4rem;
  padding: .7rem .9rem;
  border-radius: .55rem;
  background: var(--bg);
  font-size: .92rem;
  overflow-wrap: anywhere;
}
.leave-host { font-weight: 600; }
.leave-rest { color: var(--muted); }
.leave-acts { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }
/* The two that are not the primary action read as buttons rather than links,
   and clear 24px in both directions without a chip's ceremony. */
.leave-copy, .leave-stay {
  font: inherit;
  min-height: 2.5rem;
  padding: .55rem 1rem;
  /* --ui-border, not --border: this outline is the only thing saying these two
     are controls, which is the case 1.4.11 asks 3:1 for. */
  border: 1px solid var(--ui-border);
  border-radius: .55rem;
  background: transparent;
  color: var(--fg);
  cursor: pointer;
}
.leave-copy:hover, .leave-stay:hover { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--card); }
.leave-copy:focus-visible, .leave-stay:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The continue link borrows .btn, which carries a 1.4rem top margin for the
   detail page; in a row of buttons that margin is a step. */
.leave-acts .btn { margin-top: 0; margin-inline-start: auto; }
/* Narrow: stack them, each filling the row. The auto margin above parks the
   primary at the far end, which breaks once the row wraps: measured at 390px,
   Continue landed alone on a second line with dead space beside it, and at
   320px all three sat at three different widths. 44rem rather than the width
   where three buttons stop fitting: that width is a property of the words in
   them, and a breakpoint tuned to English would be wrong in six other
   languages. The order stays: column-reverse would put the primary on top while
   the keyboard still tabs copy, stay, continue, trading 2.4.3 for a layout. */
@media (max-width: 44rem) {
  /* No room beside a name at this width, and a full-width button under it read
     as the loudest thing on the page. It goes up beside the back link instead,
     the two of them at opposite ends of one row.

     Both at 2.75rem, which is the thumb target the rest of the phone layout
     uses, and both the same: 33px against 36px was close enough to look like a
     mistake rather than a hierarchy. The article's own padding already parts
     this row from the header, so the margin that stacked on top of it goes:
     22.4px and 25.6px met as 48px of nothing. */
  .detail-top { grid-template-columns: auto 1fr; row-gap: .9rem; margin-top: .4rem; }
  .detail-top .back,
  .detail-top .btn {
    min-height: 2.75rem;
    font-size: .9rem;
  }
  .detail-top .back { padding-inline: .95rem 1.05rem; }
  .detail-top .btn {
    grid-area: 1 / 1 / 2 / -1;
    justify-self: end;
    padding-inline: 1.15rem;
  }
  .detail-top .btn .btn-glyph { width: 1em; height: 1em; }
  /* The pill on a detail page stands alone and is a link to the status page, so
     it takes a thumb's worth of height. The one in a card foot keeps its size:
     that foot is dense, and raising it there would push every card down. */
  .detail-top .status-pill { min-height: 2.25rem; padding-inline: .8rem .9rem; font-size: .82rem; }
  .leave-acts { flex-direction: column; align-items: stretch; }
  .leave-acts .btn { margin-inline-start: 0; justify-content: center; }
}

/* ---- hosted pages ---- */

.page-head {
  display: flex;
  align-items: center;
  gap: .8rem;
  margin: 1.9rem 0 1.4rem;
}
.page-head h1 {
  margin: 0;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 96;
  font-weight: 470;
  font-size: clamp(1.8rem, 4vw, 2.4rem);
  letter-spacing: -.015em;
}
.page p {
  line-height: 1.75;
  margin-block: 0 1.05rem;
}
.page { padding-bottom: 2.5rem; }
.page-intro { color: var(--muted); }
.page-sec { margin-top: 2rem; }
/* The section label, worn by a hosted page's own section titles and by a
   markdown ## alike. Child, not descendant, on .detail, or it repaints the
   .page-sec titles nested inside and hands them a top margin the section
   already provides. The [lang] step is the wrapper the prose template adds when
   a field falls back to another language: without it, exactly the pages that
   need the most help rendered their headings as browser defaults. */
.page-sec h2,
:is(.detail, .detail > [lang]) > h2,
.about h2 {
  margin: 0 0 .6rem;
  font-size: .8rem;
  font-weight: 650;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--accent) 55%, var(--muted));
}
:is(.detail, .detail > [lang]) > h2, .about h2 { margin-top: 1.8rem; }

.tagline {
  margin: 2.1rem 0 0;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 40;
  font-style: italic;
  font-size: 1.25rem;
  color: var(--muted);
}
.tagline + .about { margin-top: 1.1rem; }

/* ---- rich text (markdown) ----

   Three containers, one scope. .about is the welcome note, .detail is a service
   page, and a hosted page is <article class="detail page">, so
   :is(.about, .detail) reaches all three. Everything below follows it rather
   than styling bare elements, which would leak into the header, the cards and
   the footer. */

/* Long unbreakable words push a page sideways: a linkified bare URL, a path in
   inline code, an id in a table cell. break-word, not anywhere: it breaks only
   the word that would overflow and leaves min-content widths alone, so tables
   still size themselves the way tables do. */
:is(.about, .detail) { overflow-wrap: break-word; }

/* Scoped by container, not by :is(p, li, figcaption) a: that left a link in a
   table cell or a heading with the browser's default underline and no accent,
   and it reached down into <p><a class="back">, repainting the quiet back link
   against its own rule. The three anchors named here are controls, not prose. */
:is(.about, .detail) a:not(.btn, .back, .status-pill) {
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Headings. h2 keeps the small uppercase section label set further up, which is
   what ## has rendered as all along, and the rest of the scale sits around it.
   Left to the browser, h1 came out as 2em of bold body font, louder than the
   page's own title right above it, and h6 smaller than body text. h4 down share
   one rule: a directory page has no use for four more sizes.
   No h1 rule: the renderer demotes a single # to a level two, since the page
   already has its own top heading. */
:is(.about, .detail) :is(h3, h4, h5, h6) { margin: 1.7rem 0 .5rem; line-height: 1.3; }
:is(.about, .detail) h3 { font-size: 1rem; font-weight: 650; }
:is(.about, .detail) :is(h4, h5, h6) { font-size: .95rem; font-weight: 600; color: var(--muted); }

/* The note's own padding sets its top edge: padding does not collapse with a
   child's margin, so a note opening with a heading got both one after the other
   and sat visibly lower than one opening with a paragraph. The dismiss button
   is absolutely positioned, so the first block of prose is whatever follows it,
   one step further in when the prose template wrapped a fallback language. */
.about > .about-x + *,
.about > [lang] > :first-child { margin-top: 0; }
/* Space below the last block is the card's padding to give. p:last-of-type used
   to say this: with lists and tables in the mix the last <p> is often mid-note,
   and every paragraph ending a list item or a quote matched it too. */
:is(.about, .about > [lang]) > :last-child { margin-bottom: 0; }

:is(.about, .detail) ul { margin: 0 0 .9rem; padding-inline-start: 1.15rem; }
/* Without a rule, ol kept the browser's 40px indent and sat visibly further in
   than the ul above it. Wider than the ul because the marker is drawn in this
   padding and a two-digit number needs the room. */
:is(.about, .detail) ol { margin: 0 0 .9rem; padding-inline-start: 1.6rem; }
:is(.about, .detail) li { line-height: 1.7; margin-bottom: .3rem; }
:is(.about, .detail) li::marker { color: var(--accent-ink); }
/* A nested list closes with the item that holds it: inheriting the margin-bottom
   above left a blank line inside the parent item. */
:is(.about, .detail) li :is(ul, ol) { margin-block: .25rem 0; }

/* GFM task lists. goldmark marks these with the checkbox and no class to hook,
   so the item is found by what it holds: without this the bullet stayed and the
   item read as two markers side by side. The box hangs back into the list's own
   indent, so wrapped lines line up under the text rather than under the box. */
:is(.about, .detail) li:has(> input[type="checkbox"]) { list-style: none; }
:is(.about, .detail) li > input[type="checkbox"] { margin-inline: -1.15rem .35rem; }

/* A quote reads as a quote by its rule, not by an indent: goldmark's blockquote
   arrives with the browser's bare 40px margins, so a quote after a list looked
   like one more level of it. */
:is(.about, .detail) blockquote {
  margin: 0 0 .9rem;
  padding-inline-start: 1rem;
  border-inline-start: 2px solid var(--border);
  color: var(--muted);
}
:is(.about, .detail) blockquote > :last-child { margin-bottom: 0; }

/* The browser's <hr> is a bevelled inset border and reads as the edge of a
   widget; this is the hairline the header rule and the waypoints already use. */
:is(.about, .detail) hr { margin: 1.6rem 0; border: 0; border-top: 1px solid var(--border); }

/* The chrome below is for a word inside a sentence. As a descendant selector it
   also caught every <code> inside a <pre>, and a fenced block came out as a
   stack of bordered, padded boxes, one per painted line. */
:is(.about, .detail) code {
  font-size: .88em;
  background: var(--faint);
  border: 1px solid var(--border);
  border-radius: .35em;
  padding: .08em .35em;
}
:is(.about, .detail) pre {
  margin: 0 0 1rem;
  padding: .85rem 1rem;
  background: var(--faint);
  border: 1px solid var(--border);
  border-radius: .6rem;
  /* a fenced block cannot be rewrapped, so it scrolls rather than the page */
  overflow-x: auto;
  /* Sized rather than left alone: monospace at the browser's default draws a
     visible step smaller than the prose around it. */
  font-size: .88em;
  line-height: 1.55;
}
:is(.about, .detail) pre code {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  font-size: inherit;
}

/* Tables. goldmark emits a bare <table> with no wrapper, and a table cannot
   rewrap: enough columns and it pushed the document sideways on a phone. The
   wrapper scrolls, not the table: display:block on a <table> is the usual
   one-liner and it strips the table role from the accessibility tree in Chrome
   and Safari, so the cells stop being announced as a table. The renderer emits
   this div with tabindex, because a region that scrolls has to be reachable
   from the keyboard. */
:is(.about, .detail) .table-wrap {
  max-width: 100%;
  overflow-x: auto;
  margin: 0 0 1rem;
}
:is(.about, .detail) .table-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
:is(.about, .detail) table {
  border-collapse: collapse;
  font-size: .95em;
}
:is(.about, .detail) :is(th, td) {
  padding: .45rem .7rem;
  border-bottom: 1px solid var(--border);
  /* Cells opt out of the break-word above: the table has a scroll container of
     its own, so a long path or an env var keeps its shape. Left in, one id
     squeezed its whole column into a ladder of broken syllables while the rest
     of the table sat half empty. */
  overflow-wrap: normal;
}
/* The browser centres a header cell. Only the ones GFM did not align, or this
   beats the align attribute: a presentational hint loses to any author rule. */
:is(.about, .detail) th:not([align]) { text-align: start; }
:is(.about, .detail) thead th { color: var(--muted); }

/* An image written inside a sentence carries the intrinsic width the media
   resolver measured, so a 512px screenshot ran off a 340px column. height:auto
   is the other half: cap the width and leave the height attribute alone and the
   picture comes out squashed. */
:is(.about, .detail) :is(p, li, td, th) img { max-width: 100%; height: auto; }

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

.empty { margin: 3rem 0; text-align: center; color: var(--muted); }

main { flex: 1; padding-bottom: 3.5rem; }

/* ---- footer ---- */

footer {
  border-top: 1px solid var(--border);
  margin-top: 1rem;
}
.foot {
  display: flex;
  align-items: center;
  gap: 1.2rem;
  flex-wrap: wrap;
  padding-block: 1.6rem 2.6rem;
}
.foot-mark { width: 1.1rem; height: 1.1rem; color: var(--muted); flex: none; }
.foot-links { display: flex; flex-wrap: wrap; gap: 1.3rem; }
.foot a { color: var(--muted); text-decoration: none; }
.foot a:hover, .foot a:focus-visible { color: var(--fg); }
.powered { display: inline-flex; align-items: center; gap: .45rem; margin-inline-start: auto; font-size: .82rem; color: var(--muted); }
.powered .foot-mark { width: .95rem; height: .95rem; }
.powered a { color: var(--accent-ink); font-weight: 500; }
/* the optional build stamp, quiet enough that the footer still reads as one line */
.foot-version { font-size: .95em; color: var(--muted); font-variant-numeric: tabular-nums; }
.powered a.foot-version { color: var(--muted); font-weight: 400; }
.powered a.foot-version:hover { color: var(--fg); }
.powered a:hover, .powered a:focus-visible { color: var(--accent-ink); text-decoration: underline; }

/* ---- utilities ---- */

.skip {
  position: absolute;
  inset-inline-start: -999px;
  top: .5rem;
  /* one above the sticky header: at 2 the first Tab stop was painted over and
     the skip link, whose whole job is to be seen first, was invisible */
  z-index: calc(var(--z-header) + 1);
  padding: .5rem 1rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: .5rem;
  color: var(--fg);
  text-decoration: none;
}
.skip:focus { inset-inline-start: .5rem; }
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- motion ---- */

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* back-to-top: a fixed pill, mobile only, revealed by nav.js past a screen of
   scroll. */
.totop {
  display: none;
  position: fixed;
  inset-inline-end: 1rem;
  bottom: 1rem;
  z-index: 20;
  width: 2.9rem;
  height: 2.9rem;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--card);
  color: var(--accent-ink);
  box-shadow: var(--shadow-lift);
  text-decoration: none;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .25s ease, visibility .25s ease;
}
.totop.show { opacity: 1; visibility: visible; pointer-events: auto; }
.totop svg { width: 1.15rem; height: 1.15rem; }
.totop:hover { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--card); }
.totop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* ---- mobile (below 44rem): a burger for the header links, the category trail
   as a wrapping chip row, finger-sized targets, a one-column footer, and the
   back-to-top button ---- */
@media (max-width: 44rem) {
  .wrap { padding-inline: 1.15rem; }

  /* auto-hiding sticky header: nav.js toggles .header-hidden by scroll
     direction, so a long page keeps search in reach */
  header {
    position: sticky;
    top: 0;
    z-index: var(--z-header);
    padding-block: .9rem;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    transition: transform .3s ease;
  }
  header.header-hidden { transform: translateY(-100%); }

  /* the header links fold behind a burger, the search stays beside it, and the
     desktop divider goes so the row sits centered */
  .menu { flex-wrap: nowrap; align-items: center; gap: .6rem; margin-top: .9rem; padding-block: 0; border-top: 0; }
  .nav { display: inline-flex; position: relative; flex: none; }
  .nav-burger {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    min-height: 2.75rem;
    padding: 0 .75rem;
    border: 1px solid var(--border);
    border-radius: .6rem;
    background: var(--card);
    color: var(--muted);
    font-size: .88rem;
    font-weight: 500;
    white-space: nowrap;
    cursor: pointer;
    list-style: none;
  }
  .nav-burger::-webkit-details-marker { display: none; }
  .nav-burger svg { width: 1.15rem; height: 1.15rem; flex: none; }
  .nav[open] .nav-burger { color: var(--fg); border-color: color-mix(in oklab, var(--accent) 40%, var(--border)); }
  .nav-links {
    display: flex; /* laid out (absolute, out of flow); revealed on open */
    position: absolute;
    top: calc(100% + .4rem);
    inset-inline-start: 0;
    z-index: 30;
    flex-direction: column;
    align-items: stretch;
    gap: .15rem;
    min-width: 12rem;
    padding: .4rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: .7rem;
    box-shadow: var(--shadow-lift);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-.4rem);
    pointer-events: none;
    transition: opacity .16s ease, transform .16s ease, visibility .16s;
  }
  .nav[open] .nav-links { opacity: 1; visibility: visible; transform: none; pointer-events: auto; }
  .nav-links .menu-link { display: flex; justify-content: flex-start; min-height: 2.75rem; padding: 0 .6rem; border-radius: .5rem; }
  .nav-links .menu-link:hover { background: var(--faint); }
  .search { flex: 1; min-width: 0; margin-inline-start: 0; width: auto; }
  .search:focus-within { width: auto; }

  /* finger-sized controls (44px) */
  .theme-toggle { width: 2.75rem; height: 2.75rem; }
  .langs a,
  .langs [aria-current] { display: inline-flex; align-items: center; min-height: 2.75rem; }
  .search-box input { padding-block: .55rem; }
  /* 44px for the thumb, and still a disc: the fold caps the width, so raising
     the height alone left a 30 by 44 oval with the cross 3px from the clip. */
  .about-x {
    --x-disc: 2.75rem;
    height: var(--x-disc);
    max-width: var(--x-disc);
    padding-inline: calc((var(--x-disc) - var(--x-glyph)) / 2 - 1px);
  }

  /* the category trail becomes a wrapping chip row under the intro */

  .totop { display: grid; }

  .foot { flex-direction: column; align-items: flex-start; gap: 1.1rem; padding-block: 1.6rem 2rem; }
  .foot-links { flex-direction: column; gap: .9rem; }
  .powered { margin-inline-start: 0; }
}


@media print {
  .skip, .topbar-tools, .menu, .toc, .about-x, .card-foot, .btn, .back, .powered, .totop { display: none !important; }
  body { background: #fff; }
  .about, .card { border: 1px solid #999; box-shadow: none; background: #fff; }
  .cards { grid-template-columns: 1fr; }
  .card { break-inside: avoid; }
  .card-name { text-decoration: none; }
  /* on screen this ::after is the card's invisible click overlay */
  .card-name::after {
    content: " (" attr(href) ")";
    position: static;
    inset: auto;
    font-size: .85em;
    font-weight: 400;
    color: #444;
    word-break: break-all;
  }
  .shot { break-inside: avoid; }
  /* Paper does not scroll, so whatever hung off the edge would simply be
     missing: the block wraps instead, and the table is laid out as a table. */
  :is(.about, .detail) pre { overflow-x: visible; white-space: pre-wrap; }
  :is(.about, .detail) .table-wrap { overflow-x: visible; }
  :is(.about, .detail) table { width: 100%; }
}

/* Right-to-left follows from the logical properties above; only the two literal
   arrows are turned around by hand, one glyph mirrored rather than a second
   character to keep in step. */
[dir="rtl"] .back-glyph { transform: scaleX(-1); }
