/* ==========================================================================
   SyslogStudio — project site
   --------------------------------------------------------------------------
   The palette is the application's own (frontend/src/style.css): the site
   should look like the product, not like a template that happens to mention
   it. Accents are #0969da in light and a brightened #58a6ff in dark, because
   the app's #007acc does not carry enough contrast against #1e1e1e for body
   links.

   Rules this stylesheet keeps to, deliberately:
     - hairline borders, never drop shadows, to separate surfaces
     - one accent, used for links and state, never for decoration
     - no gradient text, no glass, no floating cards
     - type carries the hierarchy; size and weight, not colour
   ========================================================================== */

/* --- tokens ------------------------------------------------------------- */

:root {
  color-scheme: light dark;

  --paper:      #ffffff;
  --paper-2:    #f6f8fa;
  --paper-3:    #eef1f4;
  --ink:        #14171a;
  --ink-2:      #3d444d;
  --ink-3:      #656d76;
  --line:       #d8dee4;
  --line-soft:  #e8ecf0;
  --accent:     #0969da;
  --accent-ink: #ffffff;
  --accent-wash:rgba(9, 105, 218, 0.07);
  --good:       #1a7f37;
  --warn:       #9a6700;
  --bad:        #cf222e;

  --font-display: "Nunito", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-body: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
    "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono",
    "Cascadia Mono", Menlo, Consolas, monospace;

  --measure: 68ch;
  --page: 1140px;
  --radius: 6px;
}

/* The dark palette, and the two ways to arrive at it.
 *
 * It is written out twice, which is deliberate and not an oversight. A reader
 * gets dark either because their SYSTEM says so and they have not overridden it
 * — the media query — or because they picked it here, which stamps
 * data-theme="dark" on <html>. Those are two different selectors and one of them
 * lives inside a media query, so no selector list can carry both.
 *
 * The `:not([data-theme="light"])` is what makes the override work in the
 * awkward direction: choosing LIGHT on a machine set to dark.
 *
 * light-dark() would collapse the two into one block. It is deliberately not
 * used: on a browser that does not know the function the whole declaration is
 * invalid at computed-value time, so the page does not fall back to light — it
 * falls back to no colour at all. Sixteen duplicated lines are the cheaper
 * failure. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:      #1e1e1e;
    --paper-2:    #262626;
    --paper-3:    #2e2e2e;
    --ink:        #e6edf3;
    --ink-2:      #c2ccd6;
    --ink-3:      #8b949e;
    --line:       #3a3a3a;
    --line-soft:  #2e2e2e;
    --accent:     #58a6ff;
    --accent-ink: #0b1a2b;
    --accent-wash:rgba(88, 166, 255, 0.10);
    --good:       #3fb950;
    --warn:       #d29922;
    --bad:        #f85149;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --paper:      #1e1e1e;
  --paper-2:    #262626;
  --paper-3:    #2e2e2e;
  --ink:        #e6edf3;
  --ink-2:      #c2ccd6;
  --ink-3:      #8b949e;
  --line:       #3a3a3a;
  --line-soft:  #2e2e2e;
  --accent:     #58a6ff;
  --accent-ink: #0b1a2b;
  --accent-wash:rgba(88, 166, 255, 0.10);
  --good:       #3fb950;
  --warn:       #d29922;
  --bad:        #f85149;
}

/* Said explicitly, so form controls and scrollbars follow the CHOICE too. With
 * only `color-scheme: light dark` on :root they would keep tracking the
 * operating system while everything painted by these tokens tracked the
 * toggle — a light page with dark select menus. */
:root[data-theme="light"] { color-scheme: light; }

/* --- reset -------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 5.5rem;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.65;
  font-feature-settings: "kern" 1;
  text-rendering: optimizeLegibility;
}

img { max-width: 100%; height: auto; display: block; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { text-decoration-thickness: 2px; }

hr { border: 0; border-top: 1px solid var(--line-soft); margin: 3rem 0; }

/* A 7 % wash behind text that keeps its own colour is not a selection, it is a
 * faint tint — measured against both palettes, it was almost invisible in dark.
 * color-mix gives a band strong enough to see and weak enough not to shout, and
 * the foreground is stated so the contrast holds in both themes. */
::selection {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  color: var(--ink);
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* --- type --------------------------------------------------------------- */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.2;
  letter-spacing: -0.015em;
  margin: 0 0 0.5em;
  font-weight: 700;
}

h1 { font-size: clamp(2rem, 4.2vw, 2.75rem); }
h2 { font-size: 1.55rem; margin-top: 2.75rem; }
h3 { font-size: 1.15rem; margin-top: 2rem; font-weight: 700; }
h4 { font-size: 1rem; margin-top: 1.5rem; }

p { margin: 0 0 1.1em; max-width: var(--measure); }
ul, ol { max-width: var(--measure); padding-left: 1.25rem; }
li { margin: 0.3em 0; }

.lede {
  font-size: 1.15rem;
  color: var(--ink-2);
  max-width: 58ch;
}

.muted { color: var(--ink-3); }
.small { font-size: 0.875rem; }

code, kbd, samp, pre { font-family: var(--font-mono); }

code {
  font-size: 0.875em;
  background: var(--paper-2);
  border: 1px solid var(--line-soft);
  border-radius: 4px;
  padding: 0.12em 0.38em;
}

pre {
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.15rem;
  overflow-x: auto;
  font-size: 0.85rem;
  line-height: 1.6;
  margin: 0 0 1.25rem;
}

pre code { background: none; border: 0; padding: 0; font-size: inherit; }

kbd {
  font-size: 0.78em;
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 4px;
  padding: 0.15em 0.42em;
  white-space: nowrap;
  color: var(--ink-2);
}

/* --- layout ------------------------------------------------------------- */

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

.wrap-narrow { max-width: 820px; }

.section { padding-block: 4.5rem; }
.section + .section { border-top: 1px solid var(--line-soft); }

.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 0.9rem;
}

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

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(180%) blur(8px);
  border-bottom: 1px solid var(--line-soft);
}

.site-header .wrap {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  min-height: 60px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-decoration: none;
  flex: none;
}

.brand img { width: 26px; height: 26px; }

.nav {
  display: flex;
  align-items: center;
  gap: 1.35rem;
  margin-left: auto;
  font-size: 0.925rem;
}

.nav a {
  color: var(--ink-2);
  text-decoration: none;
  padding-block: 0.35rem;
  border-bottom: 2px solid transparent;
}

.nav a:hover { color: var(--ink); }
.nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }

.nav .gh {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.3rem 0.7rem;
  border-bottom-width: 1px;
}

.nav .gh:hover { border-color: var(--ink-3); }
.nav .gh svg { width: 16px; height: 16px; fill: currentColor; }

@media (max-width: 860px) {
  .nav { gap: 0.9rem; font-size: 0.875rem; }
  .nav .hide-sm { display: none; }
}

/* --- buttons ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font: inherit;
  font-weight: 600;
  font-size: 0.95rem;
  padding: 0.6rem 1.15rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}

.btn:hover { border-color: var(--ink-3); }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* Not filter: brightness(). It lightens the DARK accent towards white, which
 * washes it out rather than strengthening it, and it re-rasterises the label —
 * so the text visibly shifts on hover. Mixing towards ink darkens in light and
 * towards paper lightens in dark, which is the same gesture in both. */
.btn-primary:hover {
  background: color-mix(in srgb, var(--accent) 86%, var(--ink));
  border-color: var(--accent);
}

.btn svg { width: 16px; height: 16px; fill: currentColor; flex: none; }

.btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }

/* --- hero --------------------------------------------------------------- */

.hero { padding-block: 5rem 3.5rem; }

.hero h1 {
  max-width: 16ch;
  margin-bottom: 0.35em;
}

.hero .lede { margin-bottom: 1.75rem; }

.hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin-top: 1.75rem;
  font-size: 0.85rem;
  color: var(--ink-3);
  font-family: var(--font-mono);
}

.hero-meta span { display: inline-flex; align-items: center; gap: 0.4rem; }

/* --- screenshot frame --------------------------------------------------- */

.shot {
  /* The overlay button needs a containing block. .clip already had one for
     .clip-play; this one had none, so the zoom control would have positioned
     itself against the viewport. */
  position: relative;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--paper-2);
  margin: 0;
}

.shot img { width: 100%; display: block; }

/* Theme-paired screenshots.
 *
 * Every capture exists twice, dark and light, and the page shows the one that
 * matches the palette around it — a dark screenshot on a light page is the only
 * thing on it that is not the reader's own theme, and it reads as a stock photo.
 *
 * Two <img> toggled by CSS, NOT <picture> with
 * `media="(prefers-color-scheme: dark)"`. A source's media query is evaluated
 * against the system, once, and never re-evaluated for an attribute on <html> —
 * so with <picture> the toggle in the header would repaint the whole page and
 * leave the screenshots showing the other theme.
 *
 * The hidden one is `loading="lazy"` in the markup, and a lazy image with no
 * rendering box is not fetched — so this costs one image, not two. */
.shot-light { display: block; }
.shot-dark  { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .shot-light { display: none; }
  :root:not([data-theme="light"]) .shot-dark  { display: block; }
}

:root[data-theme="dark"] .shot-light { display: none; }
:root[data-theme="dark"] .shot-dark  { display: block; }
:root[data-theme="light"] .shot-light { display: block; }
:root[data-theme="light"] .shot-dark  { display: none; }

/* --- theme toggle ------------------------------------------------------- */

/* Three states, not two: a reader who has never chosen anything should keep
 * following their system, and there is no way to return to that from a
 * two-position switch. The same hairline vocabulary as the rest of the header,
 * so it reads as part of the furniture rather than as a widget. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper-2);
}

.theme-toggle button {
  display: grid;
  place-items: center;
  width: 26px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--ink-3);
  cursor: pointer;
  transition: color .12s ease, background-color .12s ease;
}

.theme-toggle button:hover { color: var(--ink); }
.theme-toggle button svg { width: 14px; height: 14px; fill: currentColor; }

.theme-toggle button[aria-pressed="true"] {
  background: var(--paper);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--line-soft);
}

/* Until the script has run, no button is pressed and the control would flash
 * three identical states. It is revealed by the script instead. */
/* The `hidden` attribute must win.
 *
 * The browser's own rule for it is [hidden] { display: none }, which any
 * author rule that sets `display` on a class outranks — so a .badge or a
 * .theme-toggle marked hidden stayed on screen. That is how all three
 * "your platform" badges came to show at once on the download page, which
 * is worse than showing none: it tells the reader something untrue.
 *
 * !important is right here rather than a longer selector: `hidden` means
 * hidden, and a per-component override would only be rediscovered the next
 * time somebody adds a component. */
[hidden] { display: none !important; }

.shot figcaption {
  border-top: 1px solid var(--line);
  padding: 0.6rem 0.9rem;
  font-size: 0.8rem;
  color: var(--ink-3);
  background: var(--paper);
}

.shot-bar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.8rem;
  border-bottom: 1px solid var(--line);
  background: var(--paper-3);
}

.shot-bar i {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--line);
  display: block;
}

.shot-bar b {
  margin-left: 0.5rem;
  font: 500 0.72rem/1 var(--font-mono);
  color: var(--ink-3);
}

/* --- grids -------------------------------------------------------------- */

.grid { display: grid; gap: 1.5rem; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

.feature {
  border-top: 2px solid var(--line);
  padding-top: 1rem;
}

.feature h3 { margin: 0 0 0.4rem; font-size: 1.02rem; }
.feature p { margin: 0; font-size: 0.925rem; color: var(--ink-2); max-width: none; }

/* --- panels ------------------------------------------------------------- */

.panel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-2);
  padding: 1.35rem 1.5rem;
}

.panel h3 { margin-top: 0; }
.panel > :last-child { margin-bottom: 0; }

.note {
  border-left: 3px solid var(--accent);
  background: var(--accent-wash);
  padding: 0.9rem 1.1rem;
  border-radius: 0 var(--radius) var(--radius) 0;
  margin: 0 0 1.5rem;
  max-width: var(--measure);
}

.note > :last-child { margin-bottom: 0; }
.note strong { font-weight: 700; }

.note-warn { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, transparent); }

/* --- tables ------------------------------------------------------------- */

.table-scroll { overflow-x: auto; margin: 0 0 1.5rem; }

table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.9rem;
}

th, td {
  text-align: left;
  padding: 0.55rem 0.85rem;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}

th {
  font-weight: 700;
  color: var(--ink-2);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

tbody tr:last-child td { border-bottom: 0; }

/* --- documentation layout ----------------------------------------------- */

.docs {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  gap: 3.5rem;
  align-items: start;
  padding-block: 3rem 5rem;
}

.toc {
  position: sticky;
  top: 5rem;
  max-height: calc(100vh - 7rem);
  overflow-y: auto;
  font-size: 0.875rem;
  border-left: 1px solid var(--line-soft);
  padding-left: 1rem;
}

.toc strong {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 1.4rem 0 0.5rem;
  font-weight: 500;
}

.toc strong:first-child { margin-top: 0; }

.toc a {
  display: block;
  color: var(--ink-2);
  text-decoration: none;
  padding: 0.18rem 0;
  border-left: 2px solid transparent;
  margin-left: -1rem;
  padding-left: 1rem;
}

.toc a:hover { color: var(--ink); }
.toc a.current { color: var(--accent); border-left-color: var(--accent); }

.doc-body > section { scroll-margin-top: 5.5rem; }
.doc-body h2:first-child { margin-top: 0; }

@media (max-width: 900px) {
  .docs { grid-template-columns: 1fr; gap: 2rem; }
  .toc {
    position: static;
    max-height: none;
    border-left: 0;
    border-bottom: 1px solid var(--line);
    padding-left: 0;
    padding-bottom: 1.25rem;
    columns: 2;
  }
  .toc a { margin-left: 0; padding-left: 0; border-left: 0; }
}

/* --- github widgets ----------------------------------------------------- */

.repo-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin: 0 0 1.5rem;
}

.repo-stats div {
  flex: 1 1 auto;
  min-width: 118px;
  padding: 0.85rem 1.1rem;
  border-right: 1px solid var(--line-soft);
}

.repo-stats div:last-child { border-right: 0; }

.repo-stats dt {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 0.15rem;
}

.repo-stats dd {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.3rem;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.release-card { margin: 0 0 1.5rem; }

.release-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.75rem;
  margin-bottom: 0.35rem;
}

/* The permalink beside a release heading.
 *
 * Quiet until the heading is hovered or the link itself is focused, because it
 * is a tool rather than content — but it must be REACHABLE, which is the whole
 * point: the ids existed and no reader could obtain one except through
 * DevTools, which is most of why the wrong id ("vv1.5.0") went unnoticed. It
 * stays visible on keyboard focus, so it is not a mouse-only affordance. */
.release-head h2 { position: relative; }
.release-head h2 .anchor {
  /* Out of flow. At opacity 0 it still occupied its inline box and pushed the
     release date 20 px to the right — measured, before and after. Absolute at
     left:100% puts it immediately after the heading text, inside the 0.75rem
     flex gap, so nothing moves whether it is visible or not. */
  position: absolute;
  left: 100%;
  top: 0;
  margin-left: 0.15rem;
  font-weight: 600;
  text-decoration: none;
  color: var(--muted);
  opacity: 0;
  transition: opacity 0.12s ease;
}
.release-head:hover h2 .anchor,
.release-head h2 .anchor:focus-visible {
  opacity: 1;
}
@media (hover: none) {
  /* No hover to reveal it, so it simply stays. */
  .release-head h2 .anchor { opacity: 0.55; }
}
@media (prefers-reduced-motion: reduce) {
  .release-head h2 .anchor { transition: none; }
}

.tag {
  font-family: var(--font-mono);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink);
}

.asset-list { list-style: none; padding: 0; margin: 1rem 0 0; max-width: none; }

.asset-list li {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--line-soft);
  font-size: 0.9rem;
  margin: 0;
}

.asset-list a { font-family: var(--font-mono); font-size: 0.85rem; word-break: break-all; }
.asset-list .sz { margin-left: auto; color: var(--ink-3); font-size: 0.8rem; white-space: nowrap; font-variant-numeric: tabular-nums; }

.contributors { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0; margin: 0; list-style: none; }
.contributors li { margin: 0; }
.contributors img {
  width: 40px; height: 40px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--paper-2);
}

.skeleton {
  color: transparent;
  background: var(--paper-3);
  border-radius: 3px;
  animation: pulse 1.4s ease-in-out infinite;
  user-select: none;
  /* Its own width, because the text no longer supplies one. The placeholder
   * used to be "0000" and "v0.0.0" — junk that sized the box and was SERVED, so
   * a crawler and anyone without JavaScript read a version number that was not
   * a version. The text is the honest fallback now, and the pulse is sized
   * here. */
  display: inline-block;
  min-width: 3.5ch;
}

@keyframes pulse { 50% { opacity: 0.45; } }

.js-fallback { font-size: 0.9rem; color: var(--ink-3); }

/* --- badges ------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  color: var(--ink-2);
  white-space: nowrap;
}

.badge-good { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, var(--line)); }

.pill-row { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 1.5rem; }

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

.site-footer {
  border-top: 1px solid var(--line-soft);
  background: var(--paper-2);
  padding-block: 3rem 2.5rem;
  margin-top: 4rem;
  font-size: 0.9rem;
}

.footer-grid {
  display: grid;
  grid-template-columns: 1.4fr repeat(3, 1fr);
  gap: 2rem;
}

/* h2, not h4: these are top-level groupings inside the contentinfo
   landmark, and every content page jumped straight from an h2 to them —
   a document outline with a hole in it, which is the standard this site
   states on download.html. This block already sets the size, so it wins
   on specificity over the global h2 and nothing moves visually. */
.footer-grid h2 {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 500;
  margin: 0 0 0.75rem;
}

.footer-grid ul { list-style: none; padding: 0; margin: 0; }
.footer-grid li { margin: 0.35rem 0; }
.footer-grid a { color: var(--ink-2); text-decoration: none; }
.footer-grid a:hover { color: var(--accent); text-decoration: underline; }

.colophon {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  color: var(--ink-3);
  font-size: 0.85rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  justify-content: space-between;
}

.colophon p { margin: 0; max-width: none; }

@media (max-width: 760px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }
}

/* --- utility ------------------------------------------------------------ */

.stack > * + * { margin-top: 1.25rem; }
.center { text-align: center; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* A caption needs the display override to go with it.
 *
 * `position: absolute` does not take a `display: table-caption` box out of the
 * table's layout: the browser keeps a caption row, so the table dropped ~18 px
 * and grew a stray rule above its header. Measured before and after, on
 * contributing.html — and it was already true of the one caption that predates
 * this, at documentation.html, where nobody had looked. */
caption.visually-hidden { display: block; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 0.6rem 1rem;
  z-index: 100;
  border-radius: 0 0 var(--radius) 0;
}

.skip-link:focus { left: 0; }

/* --- copy buttons on code blocks ---------------------------------------- */

/* Every <pre> here is a command meant to be run. The button is inserted by
 * site.js and positioned against a wrapper it also inserts — putting it inside
 * the <pre> would make it part of what gets copied, and putting it in the
 * <pre>'s existing parent would anchor it to the section rather than the block.
 *
 * It only appears on hover or focus, because eight of these down a page is a
 * column of buttons competing with the text they belong to. Focus is not a
 * nicety: without it the button is reachable by keyboard and invisible. */
.copy-wrap {
  position: relative;
}

.copy-btn {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: 0.2rem 0.5rem;
  font: 500 0.72rem/1.5 var(--font-body);
  color: var(--ink-3);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 5px;
  cursor: pointer;
  opacity: 0;
  transition: opacity .12s ease, color .12s ease, border-color .12s ease;
}

.copy-wrap:hover .copy-btn,
.copy-btn:focus-visible { opacity: 1; }

.copy-btn:hover { color: var(--ink); border-color: var(--ink-3); }
.copy-btn.is-done { color: var(--good); border-color: var(--good); opacity: 1; }

/* A pointer is not the only way in. On a touch screen there is no hover, so the
 * button would never appear at all. */
@media (hover: none) {
  .copy-btn { opacity: 1; }
}

/* A card that describes a workspace in five lines and then dead-ends is a page
 * the reader leaves. Quiet by default, because nine of these in a grid would
 * otherwise be nine competing calls to action. */
.feature-more {
  margin: 0.9rem 0 0;
  font-size: 0.82rem;
}

.feature-more a {
  color: var(--ink-3);
  text-decoration: none;
}

.feature-more a:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* --- cropped screenshots ------------------------------------------------- */

/* A window onto a full-window capture.
 *
 * A whole 1600x1000 window scaled into a 348 px card is a grey rectangle: the
 * point of a screenshot is that you can read what is in it, and at that size
 * nobody can. So the image is NOT scaled — it is rendered at 1600 CSS pixels,
 * which is the application's own logical width, and the crop box shows one
 * region of it at 1:1. The file behind it is the 2000 px rung, so the pixels are
 * still denser than the CSS asks for.
 *
 * --cx/--cy are the top-left of the region in the application's own 1600x1000
 * coordinates — the same numbers the scene catalogue uses — so a coordinate here
 * can be checked against the interface rather than guessed at.
 *
 * `max-width: none` is not optional: the global `img { max-width: 100% }` would
 * otherwise shrink the image back down to the card and undo all of it. */
.crop {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--paper-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.crop img {
  position: absolute;
  width: 1600px;
  max-width: none;
  left: calc(var(--cx, 0) * -1px);
  top: calc(var(--cy, 0) * -1px);
}

/* The 2px rule is what gives a text-only card its top edge. When there is a
 * picture, the picture is the top edge. `:has()` keeps the markup change purely
 * additive — insert a .crop and the rule takes itself away. */
.feature > .crop { margin: 0 0 1rem; }
.feature:has(> .crop) { border-top: 0; padding-top: 0; }

/* --- the loop, in four pictures ------------------------------------------ */

.loop {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  /* `ul, ol { max-width: var(--measure) }` above is a reading-width rule for
   * prose lists, and it is right for those. This is a grid of pictures, and it
   * was being squeezed into 68ch — 587 px, which auto-fit then resolved to two
   * columns instead of four. */
  max-width: none;
  gap: 1.75rem 1.5rem;
  list-style: none;
  padding: 0;
  margin: 0;
  counter-reset: loop;
}

.loop li { counter-increment: loop; }

.loop h3 {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0.9rem 0 0.35rem;
  font-size: 1rem;
}

/* The step number, from the counter rather than from the markup — so reordering
 * the four cannot leave a "3" above the second one. */
.loop h3::before {
  content: counter(loop);
  font: 600 0.72rem/1 var(--font-mono);
  color: var(--accent);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.3rem 0.5rem;
}

.loop p {
  margin: 0;
  font-size: 0.86rem;
  color: var(--ink-2);
}

/* --- clips --------------------------------------------------------------- */

/* Some things a still cannot say: that a flat list and a table are the same 155
 * varbinds, or that every address on screen changed at once and consistently.
 *
 * `preload="none"` and no autoplay until it is on screen — a page that leads
 * with pictures still has to load. The poster is the LAST frame, so what stands
 * there before playback is the result rather than an empty starting state. */
.clip {
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--paper-2);
  margin: 0;
}

.clip video {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-3);
}

.clip figcaption {
  border-top: 1px solid var(--line);
  padding: 0.6rem 0.9rem;
  font-size: 0.8rem;
  color: var(--ink-3);
  background: var(--paper);
}

/* The same theme pairing as the stills: one clip is in the document, and which
 * one depends on the palette around it. */
.clip-light { display: block; }
.clip-dark { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .clip-light { display: none; }
  :root:not([data-theme="light"]) .clip-dark { display: block; }
}

:root[data-theme="dark"] .clip-light { display: none; }
:root[data-theme="dark"] .clip-dark { display: block; }
:root[data-theme="light"] .clip-light { display: block; }
:root[data-theme="light"] .clip-dark { display: none; }


/* --- typographic polish -------------------------------------------------- */

/* `balance` on headings, which are short enough for the browser to solve, and
 * `pretty` on the running text, which only prevents the last-line orphan. Both
 * degrade to nothing at all where they are not supported. */
h1, h2, h3, .lede { text-wrap: balance; }
p, li, figcaption { text-wrap: pretty; }

/* Checkboxes and radios in the reader's accent rather than the browser's. */
:root { accent-color: var(--accent); }

/* Arriving on a deep link should CONFIRM that you arrived. The documentation
 * has twenty-three of them and none of it said anything: the page jumped and
 * left you looking at a heading identical to every other heading. */
.doc-body > section:target > h2 {
  position: relative;
}

.doc-body > section:target > h2::before {
  content: "";
  position: absolute;
  left: -1rem;
  top: 0.15em;
  bottom: 0.15em;
  width: 3px;
  border-radius: 2px;
  background: var(--accent);
}

/* Only the scrollers that are actually ours. A page-level scrollbar rule
 * overrides a choice the reader made in their operating system. */
.doc-toc, .copy-wrap pre, .release-notes {
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

/* --- reveal on scroll ---------------------------------------------------- */

/* Deliberately small: 8 px and 240 ms, once, and only on the landing page's
 * sections. Anything more on a page of technical writing is decoration, and
 * decoration is what the rest of this stylesheet is written to avoid.
 *
 * The @supports gate is not caution for its own sake — without it, a browser
 * that parses `animation-timeline` as invalid keeps the `opacity: 0` and the
 * page is BLANK. Inside the gate, a browser that does not support the property
 * never applies the rule at all.
 *
 * `animation-range` stops it at 40 % of the entry so a section is fully solid
 * well before it reaches reading position, rather than fading as you read it. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .home .section > .wrap,
    .home .hero,
    .home .shot {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 22%;
    }

    @keyframes rise {
      from { opacity: 0; transform: translateY(8px); }
      to { opacity: 1; transform: none; }
    }
  }
}

/* --- zoom ---------------------------------------------------------------- */

/* A screenshot of an interface is a picture whose detail IS the content, and at
 * the width of a two-column grid you can see that there is a table but not what
 * is in it. Clicking one opens it at the size of the window. */
figure.shot,
figure.clip {
  cursor: zoom-in;
}

/* The focus ring follows the thing that is actually focusable.
 *
 * It used to sit on the figure, which carried tabIndex and role="button". Both
 * are gone — a figcaption must be the caption of a figure, and a real <button>
 * nested inside one that only claimed to be a button was nested-interactive —
 * so without moving this, the new control would have no visible focus at all. */
.zoom-open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* An invisible overlay covering the media, so the whole picture stays the
 * click target it has always been. It sits under .clip-play, which is painted
 * later in the DOM, so a clip offers both: play in place, or enlarge. */
.zoom-open {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  background: none;
  cursor: zoom-in;
}

/* The caption is lifted over the overlay so it stays selectable text and any
   link inside it stays clickable. It is the only child that must not be
   covered; the media below it is exactly what the control is for. */
figure.shot > figcaption,
figure.clip > figcaption {
  position: relative;
  z-index: 1;
}

.zoom {
  width: min(96vw, 1800px);
  max-width: none;
  max-height: 94vh;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper);
  color: var(--ink);
  overflow: hidden;
}

.zoom::backdrop {
  /* Not a token: ::backdrop is outside the document tree and custom properties
   * declared on :root do not reach it in every engine. */
  background: rgba(0, 0, 0, 0.72);
}

.zoom-body {
  max-height: 94vh;
  overflow: auto;
}

.zoom-body img,
.zoom-body video {
  display: block;
  width: 100%;
  height: auto;
}

.zoom-caption {
  margin: 0;
  max-width: none;
  padding: 0.7rem 1rem;
  border-top: 1px solid var(--line);
  font-size: 0.85rem;
  color: var(--ink-3);
  background: var(--paper-2);
}

.zoom-close {
  position: absolute;
  top: 0.6rem;
  right: 0.75rem;
  z-index: 1;
  width: 32px;
  height: 32px;
  font-size: 1.35rem;
  line-height: 1;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}

.zoom-close:hover { border-color: var(--ink-3); }

/* --- the demo band ------------------------------------------------------- */

/* The one section on the page that asks for something rather than explaining
 * something. A tint rather than a border, so it reads as a different KIND of
 * block without introducing a second card style to the page. */
.demo-band {
  background: var(--accent-wash);
  border-block: 1px solid var(--line-soft);
}

/* --- the demo page ------------------------------------------------------- */

/* Full-bleed tint, inset content. The band is the width of the window because
 * the frame below it is; a tinted rectangle stopping short of both edges reads
 * as a card that failed to lay out. */
.demo-strip {
  border-bottom: 1px solid var(--line-soft);
  background: var(--accent-wash);
}

.demo-strip-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding-block: 1.1rem;
}

.demo-strip p { margin: 0; max-width: 72ch; font-size: 0.9rem; }

/* Scoped down to the strip's own scale. The page needs an h1 — every other page
 * has one, and the sitemap ranks this one level with the documentation — but it
 * is a label above a frame, not a page title, and at the global clamp it would
 * be taller than the strip it sits in. */
.demo-strip h1 {
  margin: 0 0 0.25rem;
  font-size: 1.05rem;
  line-height: 1.3;
}

/* The frame takes what is left of the viewport rather than a fixed height: the
 * application lays itself out against the space it is given, and a short frame
 * makes it look cramped in a way the product is not.
 *
 * `min-height` rather than `height`, so a small laptop still gets a usable
 * window and simply scrolls. */
.demo-frame {
  display: block;
  width: 100%;
  height: calc(100vh - 190px);
  min-height: 620px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: var(--paper-2);
}

/* --- a clip that will not play by itself --------------------------------- */

/* Shown when the reader has asked for reduced motion, or when the browser
 * declines to start a muted clip. The poster is the last frame — the result —
 * so nothing is missing until they press it; this only says that there is more.
 *
 * Deliberately not `video { controls }`: a row of native player chromes changes
 * the look of the section, and someone who asked for less motion did not ask
 * for a different page. */
.clip { position: relative; }

.clip-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  padding: 0;
  color: var(--paper);
  background: color-mix(in srgb, var(--ink) 76%, transparent);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  transition: background-color .12s ease, transform .12s ease;
}

.clip-play svg { width: 24px; height: 24px; fill: currentColor; }

.clip-play:hover {
  background: var(--ink);
  transform: translate(-50%, -50%) scale(1.06);
}

.clip-play:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .clip-play { transition: none; }
  .clip-play:hover { transform: translate(-50%, -50%); }
}

/* --- the baked release notes --------------------------------------------- */

/* The changelog snapshot is raw Markdown in a <pre>, and a release note runs to
 * 663 characters on one line. `pre` does not wrap, so without this the fallback
 * — the copy that exists precisely for the reader whose scripts or network
 * failed — arrived as six screens of horizontal scrolling.
 *
 * `anywhere` rather than `break-word`: these notes carry URLs and long option
 * strings with no space to break at, which is the case `break-word` declines to
 * handle inside a pre. */
.release-notes {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
