@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;
}

: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 {
  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 a[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 {
  padding: .35rem .6rem;
  border-radius: .45rem;
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: .06em;
  text-decoration: none;
  color: var(--muted);
}
.langs-menu nav a[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;
}
.way-mark {
  width: .5rem;
  height: .5rem;
  flex: none;
  border-radius: 2px;
  background: var(--accent);
  transform: rotate(45deg);
}
.way-name {
  margin: 0;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 60;
  font-weight: 540;
  font-size: 1.5rem;
  letter-spacing: -.012em;
}
.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; }
.toc-jump { display: none; } /* shown in the mobile block */
@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;
  inset-inline-start: .2rem;
  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: .42rem;
  height: .42rem;
  flex: none;
  border-radius: 1.5px;
  background: var(--border);
  transform: rotate(45deg);
  transition: background .16s;
  position: relative;
}
.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; }

.cat { scroll-margin-top: 1.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. */
.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. */
.card-foot a.status-pill:hover { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--card); }
a.status-pill:hover { color: var(--fg); border-color: color-mix(in oklab, var(--accent) 40%, var(--border)); }
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;
}
.back-glyph { width: .85rem; height: .85rem; flex: none; }
.back:hover { color: var(--fg); border-color: color-mix(in oklab, var(--accent) 40%, var(--border)); }
.back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.detail-head { display: flex; align-items: center; gap: 1rem; margin-top: 1.6rem; }
.detail-head .tile { width: 3.6rem; height: 3.6rem; }
.detail-head .tile img { width: 2.3rem; height: 2.3rem; }
.detail-head .tile svg { width: 2rem; height: 2rem; }
.detail-title { display: flex; flex-direction: column; gap: .6rem; align-items: flex-start; }
/* 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-head 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: .8rem 1.7rem;
  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);
}
.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(--bg); }
.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) {
  .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 { border-color: color-mix(in oklab, var(--accent) 45%, var(--border)); }
.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%); }
  .cat { scroll-margin-top: 4.5rem; }

  /* 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 { 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 */
  .toc { display: block; margin: 1.5rem 0 .3rem; }
  .toc ul { display: flex; flex-wrap: wrap; gap: .5rem; }
  .toc ul::before { display: none; }
  .toc a {
    min-height: 2.5rem;
    padding: 0 .85rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--card);
    font-size: .85rem;
  }
  .toc-mark { display: none; }
  .toc a[aria-current] {
    color: var(--fg);
    font-weight: 500;
    border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
    background: color-mix(in oklab, var(--accent) 8%, var(--card));
  }

  /* Above ~7 categories the chips give way to a compact jump-to select, but
     only where something can drive it: the select's behaviour lives entirely in
     nav.js, so with JavaScript off this swap left a phone with no category
     navigation at all. The pre-paint script in <head> stamps data-js before
     first render, so gating on it costs no flash of the wrong one, and the
     fallback needs no markup: .toc.many stays a chip row. */
  [data-js] .toc.many { display: none; }
  [data-js] .toc-jump { display: block; margin: 1.5rem 0 .3rem; }
  .toc-select {
    width: 100%;
    max-width: 22rem;
    min-height: 2.75rem;
    padding: .3rem .9rem;
    /* a form control, so --ui-border and its 3:1, like the search box */
    border: 1px solid var(--ui-border);
    border-radius: .7rem;
    background: var(--card);
    color: var(--fg);
    font: inherit;
    font-size: .9rem;
  }

  .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); }
