@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:grid, 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 */
  /* The accent pushed toward black in light and toward white in dark, for
     contrast. Every focus ring but the Open button's draws in it, not in the
     accent, and so does the selected search match's border: WCAG 1.4.11 asks
     a focus indicator 3:1 against whatever it sits on, and theme.accent is
     the operator's free pick. In dark, #8a4f7d measured 2.97:1 on --bg and
     2.71:1 on --card raw, 5.30:1 and 4.83:1 through here.
     A fixed mix, not a guarantee: an accent close to black still falls short
     in dark. */
  --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: .65rem;
  min-height: 2.75rem;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 72;
  font-weight: 560;
  font-size: 1.5rem;
  letter-spacing: -.01em;
  text-decoration: none;
}
.brand img { height: 2.125rem; width: auto; }
.brand:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; border-radius: 3px; }
.foot-mark svg { display: block; width: 100%; height: 100%; }

/* The rule over the menu row runs on the content column, not across the
   .wrap's padding, and opens with the trail's diamond in the site's accent.
   Both are drawn rather than a border, which can only span the whole box. The
   row's own padding went when the links grew to 44px, or the row would have
   grown by 21px rather than 5. */
.menu {
  position: relative;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem 1.5rem;
  margin-top: .8rem;
  padding-block: .4rem 0;
}
.menu::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 2.4rem 1.5rem;
  border-top: 1px solid var(--border);
}
/* 8px, centred on the 1px rule */
.menu::after {
  content: "";
  position: absolute;
  inset-block-start: calc(-.25rem + .5px);
  inset-inline-start: 1.5rem;
  width: .5rem;
  height: .5rem;
  border-radius: 2px;
  background: var(--accent);
  transform: rotate(45deg);
}
/* A site with no header links and no search box would leave this row as an
   empty bordered strip. Children rather than :empty, because the template
   indents and a text node is not content. Same shape as the card foot. */
.menu:not(:has(> *)) { display: none; }
.menu-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  min-width: 2.75rem;
  min-height: 2.75rem;
  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-ink); 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: 2.75rem;
  height: 2.75rem;
  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-ink); 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] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  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-ink); 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;
  justify-content: center;
  gap: .25rem;
  min-width: 2.75rem;
  min-height: 2.75rem;
  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-ink); 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-ink); 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. The end padding is the button's 2.75rem, its .8rem
     inset and a gap, so the text never runs under it. */
  padding-block: 1.3rem;
  padding-inline: 1.5rem 4.2rem;
  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 note sits in a <details>, so a phone shows one row and opens it with no
   script at all. The row is drawn at every width: a browser that cannot show a
   closed fold's content keeps it everywhere, and the note is one tap away
   rather than gone. Where ::details-content exists, a wide screen and paper
   get the note whole and no row; the same query is what the stylesheet test
   reads, since hiding the row anywhere else would hide the note with it. */
.about-sum {
  display: flex;
  align-items: center;
  gap: .55rem;
  min-height: 2.75rem;
  font-size: .92rem;
  font-weight: 500;
  color: var(--fg);
  cursor: pointer;
  list-style: none;
}
.about-sum::-webkit-details-marker { display: none; }
.about-sum:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; border-radius: var(--radius); }
.about-info { width: 1.05rem; height: 1.05rem; flex: none; color: var(--accent-ink); }
.about-chev { width: .9rem; height: .9rem; flex: none; color: var(--muted); transition: transform .16s; }
.about-fold[open] .about-chev { transform: rotate(180deg); }
@supports selector(::details-content) {
  @media (min-width: 44.01rem), print {
    .about-fold::details-content { content-visibility: visible; display: contents; }
    .about-sum { display: none; }
  }
}

/* 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. 2.75rem at
     every width, since a pointer is a thumb on a touch laptop too. */
  --x-disc: 2.75rem;
  --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; }
/* Wide only. This button is absolute at the end of the folded row on a phone,
   and unfurled there it would lay its label across the row's own words. */
@media (min-width: 44.01rem) {
  .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-ink); outline-offset: 2px; }

/* 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. A search field keeps room for
   its clear button even while the button is hidden, so at 14.5rem Chromium
   left "Chercher un outil…" 117px of the 123 it takes and WebKit 113.
   At 15.5rem a Linux desktop, which draws system-ui in DejaVu Sans and names
   the shortcut "Ctrl K", left the placeholder 119.8px of the 135.8 it takes.
   ch follows a face with wide figures, but a zero is 10.7px in San Francisco
   and 10.8px in DejaVu, so the count itself covers the wider face. The cap is
   the focused width; staying under it leaves the field room to grow. */
.search {
  margin-inline-start: auto;
  width: min(25.5ch, 19rem, 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-block: 0;
  padding-inline: .85rem .3rem;
  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 that is the box's focus ring. That border
   sits between the box's fill and the page, and holds 3:1 against both. */
.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-ink);
  box-shadow: var(--shadow-lift);
}
.search-box svg { width: 1rem; height: 1rem; color: var(--muted); flex: none; }
/* The input is the 44px target, the box only its frame. */
.search-box input {
  flex: 1;
  min-width: 0;
  min-height: 2.75rem;
  padding-block: 0;
  padding-inline: .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;
}
/* The hint names a key a touch screen has none of, and on a phone its 51px are
   what the placeholder needs: "Chercher un outil…" had 109px of the 123 it
   takes at 360. */
@media (hover: none), (max-width: 44rem) { .search-kbd { display: none; } }

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

.way {
  display: flex;
  align-items: center;
  gap: .7rem;
  /* The anchor in the heading is 44px high, 2.8px past the heading's 38.4px
     line on each side, so these margins count from the anchor's box and not
     from the line of the name. */
  margin-block: 2.1rem .65rem;
  /* 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 {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  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-ink);
  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.

   Fixed, so scrolling the page never brings its foot into view: at 44px a
   link, nine categories under a 480px window left the last two below the
   screen, out of a pointer's reach and focused where no one saw the ring. It
   stops 1rem above the bottom and scrolls on its own, and focus scrolls it.
   A scroller clips what it holds at its padding box, so the .25rem all round,
   given back to top and inset, is the room the focus ring and the turned
   diamonds draw in. */
.toc { display: none; }
@media (min-width: 88rem) {
  .toc {
    display: block;
    position: fixed;
    top: 8.35rem;
    inset-inline-start: calc(50% + var(--maxw) / 2 + 1.35rem);
    max-width: 10.5rem;
    max-height: calc(100vh - 9.35rem);
    padding: .25rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    scroll-padding-block: .25rem;
  }
}
.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: 1.375rem;
  bottom: 1.375rem;
  width: 1px;
  background: var(--border);
}
/* 44px each, a pointer's target like every other link: the rail is a column
   of links and nothing else, so the height costs nothing beside the content. */
.toc a {
  display: flex;
  align-items: center;
  gap: .6rem;
  min-height: 2.75rem;
  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-ink); 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 {
    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.1rem is the gap above a heading, and
   anything past it brings the previous section's last card into view; this
   keeps 8px of that gap in hand, and the name, 6.8px down its 44px anchor,
   lands 32.2 to 32.8px from the top.

   One value, phones included, where the gap is 2rem and 6.4px stays in hand.
   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: 1.6rem; }
@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;
}
/* Two columns, the icon and the text, and the foot on a row of its own under
   both, from the content's start to its end: under the text alone it lost the
   icon's 54px, and German's two chips no longer fitted one row at 360. One
   row is declared and the foot makes the second, so a card with no foot has
   no gap under nothing, and the first row takes what the grid's stretch adds,
   which keeps the feet of a row of cards on one line. */
.card {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: 1fr;
  column-gap: .9rem;
  row-gap: .6rem;
  padding-block: 1rem .9rem;
  padding-inline: 1rem;
  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 focus stays in the
   search field, so this border is the only sign of where the keyboard is: a
   focus indicator like the ring, in --accent-ink and at 3:1 against the card
   and the page. It keeps the selection legible between beacon pulses and when
   motion is reduced. */
.card.sel {
  border-color: var(--accent-ink);
  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: contents; }

.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 scoped: the detail page's head is a grid of its own around a tile */
.card .tile { grid-column: 1; grid-row: 1; align-self: start; }
.card-text { grid-column: 2; grid-row: 1; display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
/* Two chips that do not fit one row wrap whole, the second onto a row of its
   own. The row gap is what parts the two chips' hit areas once wrapped: each
   reaches 8px past its chip, so 16px and they meet without overlapping. */
.card-foot {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: .5rem;
  row-gap: 1rem;
}
.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, as on a site with a status page for
   a service it does not monitor, is still a grid item: it would open the card's
   second row and the row gap above it, under nothing. */
.card-foot:not(:has(.flag, .status-pill)) { display: none; }

/* Host flag: an icon and its word, both shown at every width, hover or not.
   Folded to the icon at rest, the flag said nothing until a pointer found it,
   and the house and the cloud alone were read as decoration.
   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;
  min-height: 1.75rem;
  padding-block: .18rem;
  padding-inline: .5rem .6rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  color: var(--muted);
  font-size: .76rem;
  font-weight: 500;
  white-space: nowrap;
  transition: 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 { padding-inline-end: .14rem; }
.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-ink); outline-offset: 2px; }
/* 28px drawn, 44px to a thumb: the card's controls answer 8px past their
   border above and below, the offsets counted from inside the 1px border.
   Sideways a chip stops at its border, being wider than 44px already, so the
   gap between two chips stays dead. The detail glyph is 28px both ways and
   takes the 8px all round. */
a.flag::before,
.card a.status-pill::before,
.card-more::before {
  content: "";
  position: absolute;
  inset-block: calc(-.5rem - 1px);
  inset-inline: -1px;
}
.card-more::before { inset-inline: calc(-.5rem - 1px); }
@media (prefers-reduced-motion: reduce) {
  /* the dismiss button keeps the fold and loses only the travel: 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);
}

/* Balanced, so a long name does not leave one word alone on its last line,
   which is also where the sign-in lock sits. */
.card-name {
  min-width: 0;
  text-wrap: balance;
  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-ink); outline-offset: 2px; }
.card-desc { color: var(--muted); font-size: .95rem; }
/* sign_in: a lock after the name, bound to its last word so a wrapped name
   never strands it alone on a line. Raised above the stretched link only so
   its title can show on hover; a click on it still opens the service. */
.name-end { white-space: nowrap; }
.card-lock {
  position: relative;
  z-index: 1;
  display: inline-block;
  width: .8em;
  height: .8em;
  margin-inline-start: .35em;
  vertical-align: -.05em;
  color: var(--muted);
}
.card-lock svg { display: block; width: 100%; height: 100%; }

/* 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-ink); 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-ink); outline-offset: 2px; }
.card .status-pill { flex: none; min-height: 1.75rem; }
/* 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-ink); outline-offset: 2px; }
/* Drawn small on a wide screen and 44px to the pointer: the back chip measured
   72 by 33 and the detail pill 90 by 28, so each answers on a band its
   ::before draws around it. A phone and a touch screen draw them 44px tall
   instead, and the band goes. */
.back, .detail-top a.status-pill { position: relative; }
.back::before, .detail-top a.status-pill::before {
  content: "";
  position: absolute;
  inset-block: calc(-.55rem - 1px);
  inset-inline: -.2rem;
}
@media (max-width: 44rem), (hover: none) {
  .back { min-height: 2.75rem; padding-inline: .95rem 1.05rem; font-size: .9rem; }
  .back::before { display: none; }
}

/* 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 at the
   start of the line under it where there is not. The back link spans every
   column on its row, so it never 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; }
/* The sign_in note. A tinted block and no colour of its own: it is
   information, and the accent belongs to the Open button, the one action on
   the page. */
.signin {
  display: flex;
  gap: .8rem;
  align-items: flex-start;
  margin-top: 1.4rem;
  padding: .9rem 1.1rem;
  border-radius: var(--radius);
  background: var(--faint);
  color: var(--muted);
  font-size: .95rem;
}
.signin svg { width: 1.15rem; height: 1.15rem; flex: none; margin-top: .15rem; }
.signin p { margin: 0; }
.signin strong { display: block; margin-bottom: .15rem; color: var(--fg); font-weight: 600; }
.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); }
/* The way to the whole picture, under the caption on a row of its own at every
   width: on a phone the strip shows a slice of it, and the file itself is the
   only place it can be read whole. */
.shot-full {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 2.75rem;
  color: var(--accent-ink);
  font-size: .9rem;
  font-weight: 500;
  text-underline-offset: .2em;
}
.shot-full-glyph { width: .95rem; height: .95rem; flex: none; }
.shot-full:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
/* Shrunk to a phone column, the demo's 2000x700 screenshot came out 0.16 of its
   size, 120px high and unreadable. Below 44rem it keeps a readable 17rem and
   the strip scrolls sideways instead; one that fits the column at that height
   fills it at its own ratio and does not scroll.

   The height comes from the ratio the width and height attributes give the
   image. contain: size makes the image's own pixels count for nothing, so the
   strip is sized before the image loads, and the flex row is what makes every
   engine honour that ratio: without it Firefox stretched the picture. The
   template only draws the strip where cairn read both attributes, since a
   contained image with no ratio collapses.

   A screenshot is a picture of a page, not text: it reads from its left edge
   in a right-to-left page too, so the strip keeps left to right and toc.js
   can measure the fade from scrollLeft. */
@media (max-width: 44rem) {
  .shot-scroll {
    display: flex;
    overflow-x: auto;
    direction: ltr;
    border-radius: var(--radius);
  }
  .shot-scroll img {
    flex: 1 0 auto;
    contain: size;
    width: auto;
    height: auto;
    min-height: 17rem;
    max-width: none;
  }
  /* The side with more picture behind it fades, as the category trail does.
     toc.js sets the attribute; without it the strip still scrolls. */
  .shot-scroll[data-more="right"] {
    mask-image: linear-gradient(to right, #000 calc(100% - 2.2rem), transparent);
  }
  .shot-scroll[data-more="left"] {
    mask-image: linear-gradient(to right, transparent, #000 2.2rem);
  }
  .shot-scroll[data-more="left right"] {
    mask-image: linear-gradient(to right, transparent, #000 2.2rem, #000 calc(100% - 2.2rem), transparent);
  }
  /* A mask clips the element's own outline, so the fade gives way while the
     strip has the keyboard: with it the ring computed and never showed. */
  .shot-scroll:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; mask-image: none; }
}
/* A screenshot is usually a white page, which in the dark theme became the
   brightest thing on the screen. It steps down until the pointer is on it or
   the keyboard is in it. Dark arrives from the system or from the toggle, so
   both selectors carry it; light never dims. Screen only: paper is white
   whatever the screen's theme. */
.shot img { transition: filter .2s; }
@media screen and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .shot img { filter: brightness(.78); }
  :root:not([data-theme="light"]) .shot:is(:hover, :focus-within) img { filter: none; }
}
@media screen {
  :root[data-theme="dark"] .shot img { filter: brightness(.78); }
  :root[data-theme="dark"] .shot:is(:hover, :focus-within) img { filter: none; }
}
.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-ink); 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. Open leads the line under it, then
     the pill, then the state, the way a store puts its Get button under an
     app's name: up on the back link's row it sat a whole row away from the
     name it acts on. That line reads in the order Tab walks it. Open spans
     the tile's column and the next one, so it starts where the tile does; a
     service that is not open has no button, and its state takes the line
     from the tile's edge.

     Open at 2.75rem, the thumb target the rest of the phone layout uses. The
     article's own padding already parts the head 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 auto 1fr; row-gap: .9rem; margin-top: .4rem; }
  .detail-top h1 { grid-area: 2 / 2 / 3 / -1; }
  .detail-top .btn {
    grid-area: 3 / 1 / 4 / 3;
    justify-self: start;
    min-height: 2.75rem;
    padding-inline: 1.15rem;
    font-size: .9rem;
  }
  .detail-top .detail-state-row { grid-area: 3 / 3 / 4 / 4; }
  .detail-top:not(:has(.btn)) .detail-state-row { grid-area: 3 / 1 / 4 / -1; }
  .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 44px of height. The one in a card foot keeps its 28px
     and answers on 44px through its ::before: drawn taller, it would push
     every card down. */
  .detail-top .status-pill { min-height: 2.75rem; padding-inline: .8rem .9rem; font-size: .82rem; }
  .detail-top a.status-pill::before { display: none; }
  /* A line this narrow can wrap, and a divider would then open the next line
     with a stroke that separates the state from nothing: the gap alone parts
     the pill from the state here. */
  .detail-state-row .status-slot:not(:empty) + .detail-state { padding-inline-start: 0; border-inline-start: 0; }
  .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;
}
/* On a phone the rule after the title gets whatever the title leaves of the
   row: 7.6px after the demo's "Comment nous hébergeons" at 360px, a stray
   tick rather than a rule. The title stands alone there. */
@media (max-width: 44rem) {
  .page-head { margin-block: 1.2rem 1rem; }
  .page-head .way-rule { display: none; }
}
.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;
}
:is(.about, .detail) a:not(.btn, .back, .status-pill):focus-visible { outline: 2px solid var(--accent-ink); outline-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 after the fold, so the first block of prose is
   whatever follows the fold's row, one step further in when the prose template
   wrapped a fallback language. */
.about-sum + *,
.about-fold > [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-fold, .about-fold > [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-ink); 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 ---- */

/* On the content column, like the header rule; a phone keeps the full-width
   border below. */
footer { margin-top: 1rem; }
.foot {
  position: relative;
  display: flex;
  align-items: center;
  gap: 1.2rem;
  flex-wrap: wrap;
  /* the links are 44px high and carry their own air above and below */
  padding-block: 1rem 2rem;
}
.foot::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 1.5rem;
  border-top: 1px solid var(--border);
}
.foot-mark { width: 1.1rem; height: 1.1rem; color: var(--muted); flex: none; }
/* Each link is a thumb's 44px high, so a wrapped row needs no gap of its own. */
.foot-links { display: flex; flex-wrap: wrap; gap: 0 1.3rem; }
.foot-links a { display: inline-flex; align-items: center; min-height: 2.75rem; }
.foot a { color: var(--muted); text-decoration: none; }
.foot a:hover { color: var(--fg); }
/* The list and the version link read muted until they are reached; the
   credit keeps --accent-ink at rest. The keyboard's focus also draws
   cairn's ring, which the browser's own 1px one would otherwise stand in for:
   the list, the credit and the version link alike. */
.foot a:focus-visible { color: var(--fg); outline: 2px solid var(--accent-ink); outline-offset: 2px; border-radius: 3px; }
.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 { position: relative; color: var(--accent-ink); font-weight: 500; }
/* The credit keeps its place in the line and answers on 44 by 44 around it,
   centred, the way the card's chips answer past their border. Beside the
   version link the two would meet 7px apart and overlap, so there the credit's
   area grows over the words before it and the version's away from the credit,
   whatever the version's width. */
.powered a::before {
  content: "";
  position: absolute;
  inset-block: min(0px, calc(50% - 1.375rem));
  inset-inline: min(0px, calc(50% - 1.375rem));
}
.powered a:has(+ a.foot-version)::before { inset-inline: min(0px, calc(100% - 2.75rem)) 0; }
.powered a.foot-version::before { inset-inline: 0 min(0px, calc(100% - 2.75rem)); }
/* 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; }
.skip:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.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-ink); 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 brand takes what the tools leave and wraps inside it: with 44px
     controls the bar needed 322px of 324 at 360, and the languages dropped to a
     row of their own, 60px lower. A name that would take more than two lines
     there gets the whole row instead, and the tools a row under it, at its
     end: nav.js sets .topbar-split, since only a measure knows where a name
     breaks. Without it the name stays beside the tools, as many lines as it
     takes, and the header is no taller than when the tools wrapped.
     A word with no break in it keeps its own width however narrow the box:
     a host name ran 90px under the tools at 360, and a 31-letter German
     word pushed the page 3px sideways even with the whole row. Where nothing
     can give the name more room, without JavaScript and on the split row, the
     word breaks anywhere. Beside the tools with JavaScript it must not: nav.js
     reads the overflow as the cue to split, and a name that fits the whole
     row is not cut in the middle of a word. */
  .topbar { flex-wrap: nowrap; column-gap: .6rem; }
  .brand { flex: 1 1 0; min-width: 0; font-size: 1.3rem; line-height: 1.15; }
  :root:not([data-js]) .brand, .topbar-split .brand { overflow-wrap: anywhere; }
  .topbar-tools { flex: none; gap: .25rem; }
  .topbar-split { flex-wrap: wrap; row-gap: .6rem; }
  .topbar-split .brand { flex-basis: 100%; }
  .topbar-split .topbar-tools { margin-inline-start: auto; }

  /* 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; }
  .menu::before, .menu::after, .foot::before { display: none; }
  footer { border-top: 1px solid var(--border); }
  .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:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
  .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; 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; }
  /* A placeholder longer than the field ends on an ellipsis, not mid-letter:
     "Procurar uma ferramenta…" takes 174px and has 160 at 360. A wide field
     keeps the clip, which shows a letter more than an ellipsis would. */
  .search-box input { text-overflow: ellipsis; }

  /* The welcome note folds to one 44px row with the dismiss cross at its end,
     so the first card shows without scrolling: on the demo at 390 by 844 it
     sat 800px down with the note open. The open fold gets its padding back
     and pulls the row out to the box's edge, where it was when closed. */
  .tagline { margin-block-start: 1.2rem; font-size: 1.08rem; line-height: 1.45; }
  .tagline + .about { margin-block-start: .8rem; }
  .about { margin-block: .8rem 0; padding-block: 0; padding-inline: 0 2.9rem; }
  .about-sum { padding-inline: 1rem .4rem; }
  .about-fold[open] { padding-block: .2rem 1.1rem; padding-inline: 1.1rem 0; }
  .about-fold[open] .about-sum { margin-inline-start: -1.1rem; }
  .about-x { top: 0; inset-inline-end: 0; background: transparent; border-color: transparent; }
  .toc { margin-block-start: 1rem; }
  /* 2rem above a heading, with a scroll margin under it: at 1.6rem a heading
     reached from the trail landed with 6.5px of the previous card still in
     view. The first category sits closer to what is above it; .cat:first-of-type
     never matches, since the welcome note is a <section> too. */
  .way { margin-block: 2rem .8rem; }
  .cat:not(.cat ~ .cat) .way { margin-block-start: 1.3rem; }

  /* Denser cards: the foot's own row and the hosting word cost height, and
     this pays it back. On the demo at 390 no card is over 135px, against
     155px with the foot under the text alone. */
  .cards { gap: .9rem; }
  .card { padding-block: .8rem .75rem; padding-inline: .85rem; column-gap: .8rem; row-gap: .5rem; }
  .card-name { line-height: 1.35; }
  .card-text { gap: .15rem; }
  .card-desc { font-size: .92rem; line-height: 1.45; }
  .card .tile { width: 2.6rem; height: 2.6rem; border-radius: .6rem; }
  .card .tile img { width: 1.65rem; height: 1.65rem; }
  .card-foot { column-gap: .4rem; }
  .flag { padding-inline: .42rem .5rem; gap: .28rem; }
  .card .status-pill { padding-inline: .45rem .55rem; }
  .card-more { inset-block-start: .7rem; inset-inline-end: .7rem; }
  /* The glyph's 44px band reaches about 7px into the description's first
     line and up to 33px in from its end. A float one pixel tall shortens that
     line alone, so its words stay clear of the band. Physical sides from the
     page's direction, since a fallback description carries its own dir. */
  .card:has(.card-more) .card-desc::before { content: ""; float: right; width: 2.2rem; height: 1px; }
  [dir="rtl"] .card:has(.card-more) .card-desc::before { float: left; }

  /* 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; }
  .foot-links { flex-direction: column; }
  .powered { margin-inline-start: 0; }
}


@media print {
  .skip, .topbar-tools, .menu, .toc, .about-x, .card-foot, .btn, .back, .powered, .totop, .shot-full { display: none !important; }
  body { background: #fff; }
  .about, .card { border: 1px solid #999; box-shadow: none; background: #fff; }
  /* the note prints whole, so it takes back the padding a phone gave its row,
     and an opened fold drops the padding it took to sit beside that row */
  .about { padding-block: 1.3rem; padding-inline: 1.5rem; }
  .about-fold[open] { padding: 0; }
  .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 either: the strip goes and the screenshot prints at
     the column's width, whole. */
  .shot-scroll, .shot-scroll[data-more] { display: block; overflow: visible; mask-image: none; }
  .shot-scroll img { contain: none; width: 100%; height: auto; min-height: 0; }
  /* 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); }
