/*
  Quick Preview - showcase site

  The palette is not invented. Every colour below is lifted from the app's own
  theme dictionaries (src/QuickPreview.App/Themes/Dark.xaml and Light.xaml) so
  the page and the product read as one thing.

  One deliberate deviation: the app's accent (#0A84FF dark, #007AFF light) does
  not carry white label text at 4.5:1 -- white on #0A84FF is 3.65:1. So the
  accent stays for links, icons and focus rings, and buttons use a darkened
  --accent-strong (#0A6FD8, 4.91:1 with white) instead. In light mode the link
  colour is darkened for the same reason: #007AFF on the light ground is only
  3.32:1.

  Two illustrations here -- the hero scene and the keyboard -- are written in
  their FINISHED state and animate away from it. That is deliberate: with
  JavaScript off, or with animation turned off, the page still shows a preview
  window that is open and a spacebar that is lit, rather than an empty frame
  waiting for a script that is never coming.
*/

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

:root {
  /* Light, from Themes/Light.xaml */
  --ground: #e9e9ef;
  --surface: #f4f4f8;
  --card: #ffffff;
  --page: #ffffff;
  --border: rgba(0, 0, 0, 0.10);
  --border-strong: rgba(0, 0, 0, 0.18);
  --text: #1c1c1e;
  --text-muted: #3a3a42;
  --text-faint: #56565f;

  --accent: #0057b8;        /* link/text blue, 5.68:1 on --ground */
  --accent-icon: #007aff;   /* non-text only: icons, rules, focus */
  --accent-strong: #0a6fd8; /* button fill, 4.91:1 with white */
  /* Text blue, separate from the brand accent above.
     One colour cannot be both: the accent is a fill and a border, where 3:1 is
     enough, and link text needs 4.5:1 against every surface it can land on --
     and the surfaces differ. The dark accent reaches only 4.39:1 on a card and
     3.70:1 on the mock window's page, so links get their own token that clears
     all four grounds. */
  --link: #0057b8;          /* 6.87:1 on --card, 5.68:1 on --ground */
  --link-hover: #003d85;    /* 10.47:1 on --card */
  --btn-hover: #0a62bf;     /* button hover fill, 5.98:1 with white */

  /* The spacebar's halo. Non-text decoration, so the brighter accent is fine
     here where it would not be for a label. */
  --glow: rgba(0, 122, 255, 0.22);
  --glow-soft: rgba(0, 122, 255, 0.13);

  /* Syntax colours for the code illustration, measured against --page.
     The first pass reused an editor's dark-theme palette unchanged in light
     mode, where its purple was 2.78:1 and its green 3.33:1 on white. */
  --code-key: #8250a8;      /* 5.73:1 on #ffffff */
  --code-str: #2e7d32;      /* 5.13:1 on #ffffff */
  --code-fn:  #0057b8;      /* 6.87:1 on #ffffff */

  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.06);
  --shadow-window: 0 2px 8px rgba(0, 0, 0, 0.10), 0 24px 64px rgba(0, 0, 0, 0.18);

  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --space-9: 96px;

  --measure: 68ch;
  --container: 1120px;

  --font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, "Segoe UI Variable Text",
               "Segoe UI", Roboto, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", "Consolas", monospace;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  --aurora:
    radial-gradient(120% 90% at 22% 18%, #7cc4ff 0%, transparent 55%),
    radial-gradient(110% 90% at 82% 30%, #b98cff 0%, transparent 52%),
    linear-gradient(155deg, #143c6b 0%, #0b1b33 100%);

  /* Which icon the theme button shows: the one for the theme a click would
     take you to, so it reads as a destination rather than a status light. */
  --icon-sun: none;
  --icon-moon: block;
}

/*
  Dark, from Themes/Dark.xaml.

  The values appear twice on purpose. CSS cannot share one declaration body
  between a media query and a plain selector, and the two answer different
  questions: the first is "the system asked for dark and nobody overrode it",
  the second is "this visitor picked dark". Keeping the media query is what
  leaves the page theme-aware with JavaScript off, when no data-theme attribute
  is ever written at all.
*/
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  /* Dark, from Themes/Dark.xaml */
  --ground: #17171a;
  --surface: #1b1b1f;
  --card: #212127;
  --page: #2e2e34;
  --border: rgba(255, 255, 255, 0.14);
  --border-strong: rgba(255, 255, 255, 0.24);
  --text: #f5f5f7;
  --text-muted: #d0d0d8;
  --text-faint: #9a9aa4;

  --accent: #0a84ff;        /* 4.90:1 on --ground */
  --accent-icon: #0a84ff;
  --accent-strong: #0a6fd8;
  /* Link hover goes lighter on a dark ground and darker on a light one, while
     the button fill goes darker in both -- one token could not do both, and
     #1f80e8 as a button fill was only 3.95:1 with its white label. */
  --link: #3d9bff;          /* worst case 4.71:1, on --page */
  --link-hover: #66b2ff;    /* worst case 6.02:1, on --page */
  --btn-hover: #0a62bf;     /* button hover fill, 5.98:1 with white */

  --glow: rgba(10, 132, 255, 0.32);
  --glow-soft: rgba(10, 132, 255, 0.16);

  /* --page is #2e2e34 here, lighter than the ground, so the accent that
     passes on the ground (4.90:1) only reaches 3.70:1 on the card. */
  --code-key: #d6a2e8;      /* 6.55:1 on #2e2e34 */
  --code-str: #8bc98b;      /* 6.95:1 on #2e2e34 */
  --code-fn:  #4da3ff;      /* 5.14:1 on #2e2e34 */

  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3);
  --shadow-window: 0 2px 8px rgba(0, 0, 0, 0.5), 0 32px 80px rgba(0, 0, 0, 0.55);

  --icon-sun: block;
  --icon-moon: none;
  }
}

:root[data-theme="dark"] {
  /* Dark, from Themes/Dark.xaml */
  --ground: #17171a;
  --surface: #1b1b1f;
  --card: #212127;
  --page: #2e2e34;
  --border: rgba(255, 255, 255, 0.14);
  --border-strong: rgba(255, 255, 255, 0.24);
  --text: #f5f5f7;
  --text-muted: #d0d0d8;
  --text-faint: #9a9aa4;

  --accent: #0a84ff;        /* 4.90:1 on --ground */
  --accent-icon: #0a84ff;
  --accent-strong: #0a6fd8;
  /* Link hover goes lighter on a dark ground and darker on a light one, while
     the button fill goes darker in both -- one token could not do both, and
     #1f80e8 as a button fill was only 3.95:1 with its white label. */
  --link: #3d9bff;          /* worst case 4.71:1, on --page */
  --link-hover: #66b2ff;    /* worst case 6.02:1, on --page */
  --btn-hover: #0a62bf;     /* button hover fill, 5.98:1 with white */

  --glow: rgba(10, 132, 255, 0.32);
  --glow-soft: rgba(10, 132, 255, 0.16);

  /* --page is #2e2e34 here, lighter than the ground, so the accent that
     passes on the ground (4.90:1) only reaches 3.70:1 on the card. */
  --code-key: #d6a2e8;      /* 6.55:1 on #2e2e34 */
  --code-str: #8bc98b;      /* 6.95:1 on #2e2e34 */
  --code-fn:  #4da3ff;      /* 5.14:1 on #2e2e34 */

  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3);
  --shadow-window: 0 2px 8px rgba(0, 0, 0, 0.5), 0 32px 80px rgba(0, 0, 0, 0.55);

  --icon-sun: block;
  --icon-moon: none;
}

/* -------------------------------------------------------------- foundation */

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

/*
  An author `display` declaration outranks the UA stylesheet's
  `[hidden] { display: none }`, so any element that both sets a display and
  carries the attribute stays on screen. That is what was wrong with the old
  hero: hidden panels declared `display: grid`, kept rendering at full height,
  and stacked underneath the visible one inside an `overflow: hidden` box --
  which read as the preview being shifted down and cut off.
*/
[hidden] { display: none !important; }

html {
  scroll-behavior: smooth;
  /* A sticky header must not cover the heading you just jumped to. */
  scroll-padding-top: 84px;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 {
  line-height: 1.15;
  letter-spacing: -0.018em;
  margin: 0;
  text-wrap: balance;
}

h1 { font-size: clamp(2.25rem, 1.4rem + 3.4vw, 3.75rem); font-weight: 600; }
h2 { font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.35rem); font-weight: 600; }
h3 { font-size: 1.0625rem; font-weight: 600; }

p { margin: 0; }

a { color: var(--link); text-underline-offset: 3px; }
a:hover { color: var(--link-hover); }

img, svg { display: block; max-width: 100%; }

code, kbd, .mono { font-family: var(--font-mono); font-size: 0.9em; }

/* Long paths and IDs must reflow rather than force the page sideways. */
code { overflow-wrap: anywhere; }

:where(a, button, [tabindex]):focus-visible {
  outline: 2px solid var(--accent-icon);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: -100px;
  z-index: 100;
  padding: var(--space-3) var(--space-4);
  background: var(--accent-strong);
  color: #fff;
  border-radius: var(--radius);
  font-weight: 500;
  text-decoration: none;
  transition: top 120ms var(--ease);
}
.skip-link:focus { top: var(--space-4); color: #fff; }
.skip-link:hover { background: var(--btn-hover); color: #fff; }

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

.section { padding-block: var(--space-9); }
.section--tight { padding-block: var(--space-8); }

.section__head { max-width: var(--measure); margin-bottom: var(--space-7); }
.section__head p {
  margin-top: var(--space-3);
  color: var(--text-muted);
  font-size: 1.0625rem;
}
.section__head--center { margin-inline: auto; text-align: center; }

.eyebrow {
  display: block;
  margin-bottom: var(--space-3);
  color: var(--link);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.rule { border: 0; border-top: 1px solid var(--border); margin: 0; }

/*
  Text for screen readers only. The illustrations are built from empty divs and
  background gradients, so without a described alternative a screen reader
  reaches them and finds nothing at all.
*/
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

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

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  /* The opaque value first: a browser without color-mix would otherwise leave
     this transparent and the header unreadable over scrolling content. */
  background: var(--ground);
  background: color-mix(in srgb, var(--ground) 88%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  backdrop-filter: saturate(180%) blur(16px);
  border-bottom: 1px solid var(--border);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  min-height: 64px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--text);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
}
/* Monochrome: the mark carries the wordmark, not the accent. */
.brand svg { color: currentColor; }

/* The section links moved to the rail on the right, which leaves the header
   with what this is, how to get it, and which way the lights are on. Nothing
   here needs to reflow any more -- but at the narrowest widths those three do
   have to sit closer together to fit on one row. */
@media (max-width: 560px) {
  .site-header__inner { gap: var(--space-3); }
  .site-header__inner > .btn { padding-inline: var(--space-4); }
}

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 160ms var(--ease), border-color 160ms var(--ease),
              color 160ms var(--ease);
}

.btn--primary { background: var(--accent-strong); color: #fff; }
.btn--primary:hover { background: var(--btn-hover); color: #fff; }

.btn--ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--border-strong);
}
.btn--ghost:hover { background: var(--surface); color: var(--text); }

.btn--lg { min-height: 52px; padding-inline: var(--space-6); font-size: 1rem; }

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

.hero { padding-block: var(--space-8) var(--space-9); }

.hero__grid {
  display: grid;
  gap: var(--space-8);
  align-items: center;
}
@media (min-width: 1024px) {
  .hero__grid { grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); gap: var(--space-7); }
}

.hero p.lede {
  margin-top: var(--space-5);
  max-width: 46ch;
  color: var(--text-muted);
  font-size: clamp(1.0625rem, 1rem + 0.3vw, 1.1875rem);
}

/* The one fact most people want first, said before the buttons rather than
   after them. */
.badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}

.badges__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
  font-size: 0.8125rem;
  white-space: nowrap;
}

.badges__item--free {
  background: color-mix(in srgb, var(--accent-icon) 12%, var(--surface));
  border-color: color-mix(in srgb, var(--accent-icon) 40%, transparent);
  color: var(--link);
  font-weight: 600;
}
.badges__item--free svg { color: var(--accent-icon); }

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

.hero__note {
  margin-top: var(--space-4);
  color: var(--text-faint);
  font-size: 0.875rem;
}

/* ------------------------------------------------------- the hero scene --- */

/*
  A mock Explorer with the preview opening over it. Everything is positioned as
  a percentage of the stage so the whole composition scales as one drawing --
  there is no width at which the window lands somewhere different.
*/
.scene {
  width: 100%;
  max-width: 640px;
  margin-inline: auto;
  min-width: 0;
}
@media (min-width: 1024px) { .scene { max-width: none; } }

.scene__stage {
  position: relative;
  aspect-ratio: 10 / 7;
}

/* --- File Explorer, behind --- */

.xp {
  position: absolute;
  left: 0;
  top: 2%;
  width: 74%;
  height: 78%;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.xp__bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
  padding: 3.4% 4%;
  border-bottom: 1px solid var(--border);
  color: var(--text-faint);
}
.xp__nav { display: flex; gap: 6px; opacity: 0.7; }
.xp__crumb {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-muted);
  font-size: clamp(8px, 1.5vw, 12px);
  font-weight: 500;
}

.xp__grid {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 3%;
  padding: 4%;
  align-content: start;
}

.xp__tile {
  position: relative;
  display: grid;
  gap: 5px;
  justify-items: center;
  padding: 6% 3%;
  border-radius: var(--radius-sm);
  min-width: 0;
}

/* The selection is a translucent tint with a hairline, which is what Explorer
   actually draws -- a solid accent fill would make the label unreadable. */
.xp__tile--target::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-sm);
  background: rgba(0, 122, 255, 0.20);
  background: color-mix(in srgb, var(--accent-icon) 20%, transparent);
  border: 1px solid rgba(0, 122, 255, 0.55);
  border-color: color-mix(in srgb, var(--accent-icon) 55%, transparent);
}

.xp__thumb {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: 4px;
  background: linear-gradient(150deg, #6b7280 0%, #374151 100%);
}
.xp__thumb--aurora { background: linear-gradient(150deg, #7cc4ff 0%, #4c4cff 45%, #143c6b 100%); }
.xp__thumb--b { background: linear-gradient(150deg, #ffd08a 0%, #d97757 55%, #7a3b2e 100%); }
.xp__thumb--c { background: linear-gradient(150deg, #d8d8e0 0%, #9a9aa4 100%); }
.xp__thumb--d { background: linear-gradient(150deg, #8bc98b 0%, #2e7d32 100%); }
.xp__thumb--e { background: linear-gradient(150deg, #c9b6e8 0%, #7a4fa8 100%); }
.xp__thumb--f { background: linear-gradient(150deg, #ff9ec4 0%, #a03060 100%); }

.xp__label {
  position: relative;
  max-width: 100%;
  color: var(--text-muted);
  font-size: clamp(7px, 1.35vw, 11px);
  line-height: 1.3;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- the pointer and the keypress --- */

.scene__cursor {
  position: absolute;
  left: 15%;
  top: 31%;
  /* Gone by the time the window is open, which is also the state the page
     rests in when nothing is animating. */
  opacity: 0;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
  pointer-events: none;
}

.scene__key {
  position: absolute;
  left: 3%;
  top: 84%;
}
.scene__key kbd {
  display: inline-block;
  padding: 6px 14px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: clamp(9px, 1.4vw, 12px);
}

/* --- the preview window, in front --- */

.scene__window {
  position: absolute;
  left: 28%;
  top: 18%;
  width: 72%;
  height: 74%;
}

.window {
  display: flex;
  flex-direction: column;
  background: var(--page);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-window);
  overflow: hidden;
}

.window__bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
  min-height: 34px;
  padding: 2.5% var(--space-4);
  border-bottom: 1px solid var(--border);
}

.window__name {
  font-size: clamp(0.75rem, 1.6vw, 0.875rem);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.window__count {
  margin-left: auto;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: clamp(0.625rem, 1.3vw, 0.75rem);
}

.window__close {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: 0 0 24px;
  color: var(--text-faint);
  border-radius: var(--radius-sm);
}

.window__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: clamp(10px, 3%, 24px);
  overflow: hidden;
}

.window__meta {
  display: flex;
  gap: var(--space-4);
  align-items: center;
  flex: 0 0 auto;
  min-height: 28px;
  padding: 1.6% var(--space-4);
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: clamp(0.625rem, 1.3vw, 0.75rem);
  white-space: nowrap;
  overflow: hidden;
}
.window__meta .accent { color: var(--link); }

/* The stand-in photograph, shared by the hero window and the Images card. */
.art-photo {
  position: relative;
  height: 100%;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--aurora);
}

/* The Images card claims that animated GIFs play, so its preview moves. The
   drifting layer is oversized and clipped, which is what stops the edges
   showing through as it travels. */
.art-photo--drift::before {
  content: "";
  position: absolute;
  inset: -14%;
  background: var(--aurora);
  transform: translate3d(3%, 2%, 0) scale(1.04);
}

/* --- the loop ---

  Eight seconds: the preview is open, Space closes it, the pointer picks the
  file again, Space reopens it, and it holds. Closing is in the loop on purpose
  -- "press it again and it is gone" is half of the gesture and the half a
  still picture can never show.

  The cycle starts AND ends in the open state, which is also the state the
  scene rests in when nothing is animating. That is what stops the window
  blinking out of existence the moment the animation is switched on, and it is
  why the beats are ordered open-close-reopen rather than the other way round.

  Every part shares one duration and is timed in percentages of it rather than
  in delays. With separate durations the parts drift apart a little on each
  repetition; sharing the cycle means the window opens at 38% forever.

  It runs only while it is on screen, it stops entirely for anyone who has
  asked for less motion, and it pauses under the pointer -- so there is a way
  to hold it still and read it without a control sitting under the picture.
*/
.scene__stage.is-playing .xp__tile--target::before { animation: loop-select 8s linear infinite; }
.scene__stage.is-playing .scene__cursor            { animation: loop-cursor 8s var(--ease) infinite; }
.scene__stage.is-playing .scene__key kbd           { animation: loop-key 8s linear infinite; }
.scene__stage.is-playing .scene__window            { animation: loop-window 8s var(--ease) infinite; }
.scene__stage.is-playing .window__body,
.scene__stage.is-playing .window__meta             { animation: loop-content 8s linear infinite; }

.scene__stage.is-playing:hover *,
.scene__stage.is-playing:hover *::before { animation-play-state: paused; }

/* Still selected while the preview is open, and after it shuts -- Explorer
   does not drop the selection just because you closed a preview. */
@keyframes loop-select {
  0%, 17%   { opacity: 1; }
  21%, 26%  { opacity: 0; }
  30%, 100% { opacity: 1; }
}

@keyframes loop-cursor {
  0%, 19%   { opacity: 0; transform: translate(44px, 36px); }
  22%       { opacity: 1; transform: translate(32px, 26px); }
  28%, 34%  { opacity: 1; transform: translate(0, 0); }
  38%, 100% { opacity: 0; transform: translate(0, 0); }
}

/* Pressed twice: once to close at 12%, once to open again at 31%. */
@keyframes loop-key {
  0%, 10% {
    transform: translateY(0);
    border-color: var(--border-strong);
    box-shadow: none;
  }
  12% {
    transform: translateY(2px);
    border-color: var(--accent-icon);
    box-shadow: 0 0 0 4px var(--glow-soft);
  }
  15%, 29% {
    transform: translateY(0);
    border-color: var(--border-strong);
    box-shadow: none;
  }
  31% {
    transform: translateY(2px);
    border-color: var(--accent-icon);
    box-shadow: 0 0 0 4px var(--glow-soft);
  }
  34%, 100% {
    transform: translateY(0);
    border-color: var(--border-strong);
    box-shadow: none;
  }
}

/* Shrinking away on the way out, and growing from 94% on the way back in --
   which is the app's own reveal. The two scales differ, and the frames in
   between are invisible anyway. */
@keyframes loop-window {
  0%, 12%   { opacity: 1; transform: scale(1); }
  17%       { opacity: 0; transform: scale(0.96); }
  33%       { opacity: 0; transform: scale(0.94); }
  38%, 100% { opacity: 1; transform: scale(1); }
}

/* The contents land just after the frame does. */
@keyframes loop-content {
  0%, 12%   { opacity: 1; }
  16%, 36%  { opacity: 0; }
  42%, 100% { opacity: 1; }
}

@media (max-width: 640px) {
  .scene__stage { aspect-ratio: 4 / 3; }
  .scene__key kbd { padding: 4px 9px; }
}

/* ---------------------------------------------------------- the keyboard --- */

/*
  Every key it does not use, drawn once so the one it does can be the only lit
  thing on the page. The keyboard fades out toward its corners with a mask
  rather than a blur -- opacity falloff keeps the near keys crisp, and a browser
  without mask support simply gets the whole board, which is not a failure.
*/
.kb {
  --key: clamp(19px, 3.05vw, 46px);
  --key-gap: clamp(3px, 0.5vw, 7px);

  /*
    Tighter and steeper than it was. The old ellipse still showed the corner
    keys at nearly half strength, which left five rows of legible letters
    competing with the one key the section is about. This reaches full black
    over the spacebar and is gone well before the edges, so the board reads as
    a board and only the middle of the bottom row reads as keys.
  */
  --kb-mask: radial-gradient(ellipse 62% 122% at 50% 88%,
             #000 11%, rgba(0, 0, 0, 0.30) 47%, transparent 78%);

  /*
    How lit the spacebar is, 0 to 1. Driven from scroll position in script.js
    and defaulting to fully lit, so with JavaScript off, or before the first
    scroll event, the illustration is in its finished state rather than dark.
  */
  --kb-lit: 1;

  display: grid;
  gap: var(--key-gap);
  justify-items: center;
  max-width: 920px;
  margin: var(--space-7) auto var(--space-5);
  padding-block: var(--space-4) var(--space-6);
  -webkit-mask-image: var(--kb-mask);
          mask-image: var(--kb-mask);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

.kb__row { display: flex; gap: var(--key-gap); }

/*
  Real keycap widths: a 2u key covers two 1u keys and the gap between them, so
  every row lines up on the same grid the way a keyboard actually does.
*/
.kb__key {
  --u: 1;
  display: grid;
  place-items: center;
  width: calc(var(--key) * var(--u) + var(--key-gap) * (var(--u) - 1));
  height: var(--key);
  flex: 0 0 auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: calc(var(--key) * 0.22);
  color: var(--text-faint);
  font-size: clamp(8px, 1.05vw, 12px);
  font-weight: 500;
  user-select: none;
}

.kb__key--u125 { --u: 1.25; }
.kb__key--u15  { --u: 1.5; }
.kb__key--u175 { --u: 1.75; }
.kb__key--u2   { --u: 2; }
.kb__key--u225 { --u: 2.25; }
.kb__key--u275 { --u: 2.75; }

/* The named keys carry words rather than a character, so they get their own
   smaller size instead of overflowing a cap sized for one letter. */
.kb__key--u125,
.kb__key--u15,
.kb__key--u175,
.kb__key--u2,
.kb__key--u225,
.kb__key--u275 { font-size: clamp(6px, 0.78vw, 9.5px); }

/*
  The rest state is the lit one -- see the note at the top of this file. Every
  lit property is scaled by --kb-lit, so the same declarations cover the whole
  range from dark to fully lit without a second rule or a keyframe.
*/
.kb__key--space {
  --u: 6.25;
  border-color: color-mix(in srgb, var(--accent-icon) calc(var(--kb-lit) * 100%), var(--border));
  color: color-mix(in srgb, var(--text) calc(var(--kb-lit) * 100%), var(--text-faint));
  box-shadow:
    0 0 0 calc(var(--kb-lit) * 3px) var(--glow-soft),
    0 0 calc(var(--kb-lit) * 34px) calc(var(--kb-lit) * 7px) var(--glow);
}

.kb__spacelabel {
  font-size: clamp(8px, 1.05vw, 12px);
  font-weight: 600;
  opacity: calc(0.55 + var(--kb-lit) * 0.45);
}

/* Under about 820px the number row is the first thing that stops fitting, and
   it is also the row that matters least. The spacebar never goes. */
@media (max-width: 820px) { .kb__row--num { display: none; } }
@media (max-width: 560px) { .kb__key--edge { display: none; } }

.kb__caption {
  max-width: 60ch;
  margin: 0 auto var(--space-8);
  color: var(--text-muted);
  text-align: center;
}
.kb__caption kbd {
  padding: 2px 8px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  color: var(--text);
}

/* ------------------------------------------------------------------- steps */

.steps {
  display: grid;
  gap: var(--space-6);
  counter-reset: step;
}
@media (min-width: 760px) { .steps { grid-template-columns: repeat(3, 1fr); } }

.step { counter-increment: step; }
.step__num {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  margin-bottom: var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--link);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.step__num::before { content: counter(step); }
.step p { margin-top: var(--space-2); color: var(--text-muted); font-size: 0.9375rem; }

/* ----------------------------------------------------------------- formats */

/*
  Rows, not cards. A framed preview inside a framed card was two boxes deep for
  no reason -- the preview already has a frame, because it is a window. What is
  left is text, a picture, and a hairline between them.
*/
.formats { display: grid; }

.format {
  position: relative;
  padding-block: var(--space-6);
}
.format + .format { border-top: 1px solid var(--border); }
.format:hover,
.format:focus-within,
.format:has(.fmt.is-open) { z-index: 3; }

/* The disclosure panel hangs off the words, so the words are its containing
   block: that is what keeps a sixty-item list from running off the side of a
   narrow screen, whichever half of the row it is in. */
.format__text {
  position: relative;
  min-width: 0;
  margin-top: var(--space-4);
}

.format__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.format__head svg { color: var(--accent-icon); flex: 0 0 auto; }

.format p {
  margin-top: var(--space-2);
  color: var(--text-muted);
  font-size: 0.875rem;
  overflow-wrap: anywhere;
}

/*
  Wide enough for two columns: the preview takes one side and the words the
  other, and the sides swap on every row so the eye zig-zags down the section
  rather than scanning the same column seven times.
*/
@media (min-width: 880px) {
  .format {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr);
    align-items: center;
    gap: var(--space-8);
    padding-block: var(--space-7);
  }

  /* The markup leads with the preview, which is the right order when the card
     is one stacked column. Here the odd rows pull the words in front of it
     instead, and the even rows leave it alone. */
  .format:nth-child(odd) .format__text { order: -1; }

  .format__text { margin-top: 0; }
  .format__head h3 { font-size: 1.3125rem; }
  .format p { margin-top: var(--space-3); font-size: 0.9375rem; }
}

/* --- the small preview on each card --- */

/*
  Deliberately not the full window chrome from the hero. One title strip and
  the content, at a third of the size: near enough to read as the same product,
  far enough that seven of them are not seven copies of the hero.
*/
/*
  The preview is a third of the page wide in a stacked card and half of it in a
  two-column row, so its type is sized against the box rather than the viewport
  -- a container query is the only way the same drawing reads correctly at both
  sizes without a second set of numbers.
*/
.mini {
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  aspect-ratio: 16 / 9;
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.mini__bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  padding: clamp(4px, 1.5cqw, 9px) clamp(6px, 2.2cqw, 14px);
  border-bottom: 1px solid var(--border);
  font-size: clamp(9px, 2.5cqw, 13.5px);
  line-height: 1.4;
}
.mini__name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mini__pos {
  margin-left: auto;
  flex: 0 0 auto;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: clamp(8px, 2.1cqw, 11.5px);
}

.mini__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: clamp(7px, 2.4cqw, 16px);
  overflow: hidden;
}
.mini__body--flush { padding: 0; }
.mini__body--flush .art-photo { border-radius: 0; }

.mini__page { display: grid; gap: clamp(4px, 1.4cqw, 9px); align-content: start; }
.mini__line {
  display: block;
  height: clamp(4px, 1.1cqw, 6px);
  border-radius: 3px;
  background: currentColor;
  --o: 0.16;
  opacity: var(--o);
}
.mini__line--title { height: clamp(6px, 1.9cqw, 10px); width: 54%; --o: 0.36; }
.mini__line--mid { width: 86%; }
.mini__line--short { width: 64%; }

.mini__code {
  margin: 0;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: clamp(8px, 2.1cqw, 12px);
  line-height: 1.72;
  white-space: pre;
}
.mini__code .cl { display: block; }
.mini__code .ln { display: inline-block; width: 1.9em; color: var(--text-faint); user-select: none; }
.mini__code .k { color: var(--code-key); }
.mini__code .s { color: var(--code-str); }
.mini__code .f { color: var(--code-fn); }

/* One shared row height, because the folder preview's selection has to travel
   in exact multiples of it. */
.mini__rows {
  --row-gap: clamp(2px, 0.7cqw, 5px);
  --row-h: calc(clamp(9px, 2.4cqw, 13px) * 1.5 + clamp(2px, 0.7cqw, 5px) * 2);
  display: grid;
  gap: var(--row-gap);
  align-content: start;
}
.mini__row {
  display: flex;
  align-items: center;
  gap: clamp(5px, 1.8cqw, 10px);
  padding: clamp(2px, 0.7cqw, 5px) clamp(4px, 1.5cqw, 9px);
  border-radius: 4px;
  height: var(--row-h);
  font-size: clamp(9px, 2.4cqw, 13px);
  line-height: 1.5;
  white-space: nowrap;
  overflow: hidden;
}
.mini__row--nest { padding-left: 16px; }

/* The folder preview's selection, as a layer rather than a row style, so it
   can walk down the list the way the arrow keys move it. A translucent tint
   keeps the row's own text readable underneath -- which is also what Explorer
   draws. */
.mini__rows--nav { position: relative; }
.mini__rows--nav::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: var(--row-h);
  border-radius: 4px;
  background: rgba(0, 122, 255, 0.20);
  background: color-mix(in srgb, var(--accent-icon) 20%, transparent);
  border: 1px solid rgba(0, 122, 255, 0.5);
  border-color: color-mix(in srgb, var(--accent-icon) 50%, transparent);
  pointer-events: none;
}
.mini__row em {
  margin-left: auto;
  padding-left: 6px;
  font-family: var(--font-mono);
  font-size: clamp(8px, 2.1cqw, 11.5px);
  font-style: normal;
  opacity: 0.65;
}
.mini__glyph {
  flex: 0 0 auto;
  width: clamp(7px, 1.9cqw, 10px);
  height: clamp(9px, 2.5cqw, 13px);
  border-radius: 2px;
  background: currentColor;
  opacity: 0.45;
}
.mini__glyph--dir {
  width: clamp(10px, 2.7cqw, 14px);
  height: clamp(8px, 2.1cqw, 11px);
  border-radius: 2px;
  background: var(--accent-icon);
  opacity: 0.75;
}

/*
  Only shown once the preview is wide enough to hold them. This is the reason
  the mini is a container: at 860px the card is one stacked column and the
  preview is nearly the full page wide, while at 1400px it is half a row and
  much smaller -- the viewport tells you nothing about which you have.
*/
.mini__line--extra,
.mini__row--extra,
.mini__more { display: none; }

@container (min-width: 340px) {
  .mini__line--extra { display: block; }
  .mini__row--extra  { display: flex; }
  .mini__more        { display: contents; }
}

.mini__media { display: flex; align-items: center; gap: clamp(8px, 3cqw, 20px); height: 100%; }
.mini__art {
  flex: 0 0 auto;
  width: 34%;
  max-width: 150px;
  aspect-ratio: 1;
  border-radius: 4px;
  background:
    radial-gradient(90% 90% at 30% 25%, #ff9ec4 0%, transparent 60%),
    linear-gradient(150deg, #7a4fa8 0%, #241436 100%);
}
.mini__wave { display: flex; align-items: center; gap: clamp(3px, 0.9cqw, 6px); flex: 1 1 auto; height: 74%; }
.mini__wave i {
  flex: 1 1 auto;
  height: var(--h);
  border-radius: 2px;
  background: currentColor;
  --o: 0.3;
  opacity: var(--o);
  transform-origin: center;
}

.mini__caret {
  display: block;
  width: 5px;
  height: 1em;
  margin-top: 2px;
  background: currentColor;
  --o: 0.5;
  opacity: var(--o);
}

.mini__card {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 6px;
  height: 100%;
}
.mini__cardicon { color: var(--text-faint); }
.mini__cardrows { display: grid; gap: 3px; justify-items: center; width: 62%; }
.mini__cardrows i {
  display: block;
  width: 100%;
  height: clamp(3px, 0.9cqw, 5px);
  border-radius: 2px;
  background: currentColor;
  --o: 0.16;
  opacity: var(--o);
}
.mini__cardrows i:nth-child(2) { width: 76%; }
.mini__cardrows i:nth-child(3) { width: 54%; }
.mini__cardrows i:nth-child(4) { width: 88%; }
.mini__cardrows i:nth-child(5) { width: 44%; }

/* --- each preview, once --- */

/*
  A preview animates the first time its row reaches the screen and then holds.
  As with the hero, the state written in the rules above IS the finished state,
  so a preview that never animates -- no JavaScript, reduced motion, an old
  browser -- is a still picture rather than a broken one.

  Play-once rather than looping: two or three rows are on screen at any moment,
  and seven things moving forever beside the words you are trying to read is
  not liveliness, it is noise.
*/

/* A default, so that a piece which somehow reaches the page without one still
   animates. calc() on an undefined custom property is invalid, and an invalid
   value in the animation shorthand throws the whole declaration away -- which
   fails silently and looks exactly like a piece that was never meant to move. */
.mini__line, .mini__row, .cl, .mini__wave i, .mini__cardrows i { --i: 0; }

/* Documents: a page that builds up a line at a time. */
.mini.is-live .mini__line { animation: piece-in 300ms var(--ease) calc(var(--i) * 70ms) both; }

/* Text and code: line after line, with the caret waiting at the end. */
.mini.is-live .cl { animation: piece-in 240ms var(--ease) calc(var(--i) * 90ms) both; }
.mini.is-live .mini__caret { animation: piece-in 260ms var(--ease) 900ms both; }

/* Archives: entries read out of the index in order. */
.mini.is-live .mini__row { animation: row-in 300ms var(--ease) calc(var(--i) * 80ms) both; }

/* Audio: the waveform drawn up from the baseline. */
.mini.is-live .mini__wave i { animation: bar-in 420ms var(--ease) calc(var(--i) * 45ms) both; }

/* Metadata: the properties the shell hands back, as they arrive. */
.mini.is-live .mini__cardrows i { animation: piece-in 300ms var(--ease) calc(var(--i) * 90ms) both; }

/* Images: the GIF plays. */
.mini.is-live .art-photo--drift::before { animation: gif-drift 4600ms var(--ease) both; }

/* Folders: the selection walks the list and comes back, which is what holding
   an arrow key looks like. It ends where it started, on the first row. */
.mini.is-live .mini__rows--nav::before { animation: sel-walk 2800ms var(--ease) 400ms both; }

@keyframes piece-in {
  from { opacity: 0; transform: translateY(5px); }
  to   { opacity: var(--o, 1); transform: none; }
}

@keyframes row-in {
  from { opacity: 0; transform: translateX(-9px); }
  to   { opacity: var(--o, 1); transform: none; }
}

@keyframes bar-in {
  from { opacity: 0; transform: scaleY(0.05); }
  to   { opacity: var(--o, 1); transform: scaleY(1); }
}

@keyframes gif-drift {
  from { transform: translate3d(-3.5%, -2.5%, 0) scale(1.13); }
  to   { transform: translate3d(3%, 2%, 0) scale(1.04); }
}

@keyframes sel-walk {
  0%, 14%  { transform: translateY(0); }
  32%      { transform: translateY(calc(var(--row-h) + var(--row-gap))); }
  52%      { transform: translateY(calc((var(--row-h) + var(--row-gap)) * 2)); }
  72%      { transform: translateY(calc((var(--row-h) + var(--row-gap)) * 3)); }
  100%     { transform: translateY(0); }
}

/* --- "show all formats" --- */

/*
  A disclosure rather than a tooltip. Most people neither need nor recognise a
  list of sixty extensions, but the few who are looking for one specific one
  need it exactly -- so it goes behind one word at the end of the sentence.
*/
.fmt__trigger {
  display: inline;
  padding: 0;
  border: 0;
  background: none;
  color: var(--link);
  font: inherit;
  font-size: inherit;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
  cursor: pointer;
}
.fmt__trigger:hover { color: var(--link-hover); }

.fmt__panel {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: 4;
  padding: var(--space-3) var(--space-4);
  background: var(--page);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity 140ms var(--ease), transform 140ms var(--ease),
              visibility 0s linear 140ms;
}

.fmt:hover .fmt__panel,
.fmt__panel:hover,
.fmt.is-open .fmt__panel {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

.fmt__list {
  display: block;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  line-height: 1.9;
  letter-spacing: 0.02em;
  overflow-wrap: anywhere;
}

/* ----------------------------------------------------------------- privacy */

/*
  Its own band rather than another row of cards. This is the claim most likely
  to decide whether someone installs an unsigned binary from a stranger, so it
  is given the width of the page and a ground of its own.

  It used to announce that with two hairline rules and a flat slab of --surface,
  which is why it read as a section borrowed from another site: every other
  block on this page sits on --ground and is separated by space, not by a line.
  The ground is still lifted here, but it now fades in and out instead of
  starting and stopping, so the section is distinct without being detached.

  The tint lives on a pseudo-element rather than on .band itself because the
  fade is a mask, and a mask on the section would take the text with it.
*/
.band {
  position: relative;
  padding-block: var(--space-9);
  isolation: isolate;
}

.band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(72% 48% at 50% 4%,
      color-mix(in srgb, var(--accent-icon) 13%, transparent) 0%, transparent 70%),
    var(--surface);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 10%, #000 90%, transparent 100%);
          mask-image: linear-gradient(180deg, transparent 0%, #000 10%, #000 90%, transparent 100%);
}

.privacy {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
}

/*
  One card language for all seven claims. Before this the lead was a card and
  the six under it were bare icon rows, so the section argued with itself about
  what kind of thing a privacy claim was. They are all cards now, translucent
  enough that the band's tint still comes through them.
*/
.privacy__item {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: var(--space-4);
  padding: var(--space-5);
  background: color-mix(in srgb, var(--card) 55%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 180ms var(--ease), transform 180ms var(--ease);
}
.privacy__item svg { color: var(--accent-icon); margin-top: 2px; }
.privacy__item h3 { font-size: 1rem; }
.privacy__item p { margin-top: var(--space-2); color: var(--text-muted); font-size: 0.9375rem; }

@media (hover: hover) {
  .privacy__item:hover {
    border-color: color-mix(in srgb, var(--accent-icon) 42%, var(--border));
    transform: translateY(-2px);
  }
}

.privacy__item--lead {
  grid-column: 1 / -1;
  grid-template-columns: 26px 1fr;
  padding: var(--space-6);
  background: var(--card);
  border-color: var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.privacy__item--lead h3 { font-size: 1.25rem; }
.privacy__item--lead p { font-size: 1rem; max-width: 62ch; }

/* ---------------------------------------------------------------- features */

.features {
  display: grid;
  gap: var(--space-6) var(--space-7);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

.feature { display: grid; grid-template-columns: 34px 1fr; gap: var(--space-4); }
.feature svg { color: var(--accent-icon); margin-top: 2px; }
.feature p { margin-top: var(--space-2); color: var(--text-muted); font-size: 0.9375rem; }

/* ---------------------------------------------------------------- keyboard */

.keys { display: grid; gap: 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.keys__row {
  display: grid;
  grid-template-columns: minmax(140px, 210px) 1fr;
  gap: var(--space-4);
  align-items: baseline;
  padding: var(--space-4) var(--space-5);
  background: var(--card);
}
.keys__row + .keys__row { border-top: 1px solid var(--border); }
.keys__row span { color: var(--text-muted); font-size: 0.9375rem; }

@media (max-width: 560px) {
  .keys__row { grid-template-columns: 1fr; gap: var(--space-2); }
}

kbd {
  display: inline-block;
  min-width: 2em;
  padding: 3px 8px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 0.8125rem;
  text-align: center;
}

/* --------------------------------------------------------------------- faq */

.faq {
  display: grid;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.faq__item { background: var(--card); }
.faq__item + .faq__item { border-top: 1px solid var(--border); }

.faq__item summary {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: 44px;
  padding: var(--space-4) var(--space-5);
  font-weight: 500;
  cursor: pointer;
  /* Both are needed: the second is Safari's, and `display: flex` alone still
     leaves a marker in some engines. */
  list-style: none;
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary:hover { color: var(--link); }

.faq__item summary::after {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  margin-left: auto;
  border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 160ms var(--ease);
}
.faq__item[open] summary::after { transform: translateY(2px) rotate(225deg); }

.faq__body {
  padding: 0 var(--space-5) var(--space-5);
  max-width: var(--measure);
}
.faq__body p { color: var(--text-muted); font-size: 0.9375rem; }

/* ---------------------------------------------------------------- download */

.download {
  padding: var(--space-8) var(--space-6);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  text-align: center;
}
.download p { margin-top: var(--space-4); color: var(--text-muted); }

/* The argument that used to live in a second, identical card above this one.
   Held to a reading measure so it does not run the full width of the card. */
.download__lede {
  max-width: 62ch;
  margin-inline: auto;
}
.download__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
  margin-top: var(--space-6);
}
.download__req {
  margin-top: var(--space-5);
  color: var(--text-faint);
  font-size: 0.875rem;
}

/* Warmer than a requirement note and quieter than a call to action: it is an
   offer, not an ask, and the page should read the same if it is ignored. */
.download__support {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.9375rem;
}

.notice {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: var(--space-3);
  max-width: var(--measure);
  margin: var(--space-6) auto 0;
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-align: left;
}
.notice svg { color: var(--text-faint); margin-top: 2px; }
.notice p { margin: 0; color: var(--text-muted); font-size: 0.875rem; }
.notice p + p { margin-top: var(--space-2); }

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

/*
  It used to carry the settings and log paths, which the privacy section and
  the FAQ both already answer, and a pointer at a file inside the repository,
  which means nothing to somebody reading a web page. What is left is the three
  things a footer is actually for: who made this, where else to find them, and
  the notice about names that belong to other people.
*/
.site-footer {
  padding-block: var(--space-8) var(--space-6);
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: 0.875rem;
}

.site-footer__top {
  display: grid;
  gap: var(--space-6);
  padding-bottom: var(--space-6);
}
@media (min-width: 760px) {
  .site-footer__top {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--space-8);
  }
}

.site-footer__brand p {
  max-width: 42ch;
  margin-top: var(--space-3);
  line-height: 1.65;
}

.site-footer__heading {
  margin: 0 0 var(--space-4);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.site-footer__links ul {
  display: grid;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}
.site-footer__links a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 34px;
  color: var(--text-muted);
  text-decoration: none;
  transition: color 160ms var(--ease);
}
.site-footer__links a:hover { color: var(--link); }
.site-footer__links svg { flex: 0 0 auto; opacity: 0.75; }
.site-footer__links a:hover svg { opacity: 1; }

.site-footer__bar {
  display: grid;
  gap: var(--space-3);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}
@media (min-width: 900px) {
  .site-footer__bar {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    gap: var(--space-7);
  }
}
.site-footer__copy { white-space: nowrap; }
.site-footer__tm { max-width: 78ch; font-size: 0.75rem; line-height: 1.6; }

/* The floating support button gets out of the footer's way on small screens,
   where the two would otherwise sit on top of each other. */
@media (max-width: 640px) { .site-footer { padding-bottom: var(--space-9); } }

/* --------------------------------------------------- buy me a coffee ----- */

/*
  Buy Me a Coffee's own yellow rather than the site palette. It is the one
  element on the page that is deliberately not part of the design system: it
  has to be recognisable at a glance and it has to survive being scrolled past
  a dozen times. #111 on #FFDD00 is 17:1.
*/
.bmc {
  position: fixed;
  right: var(--space-5);
  bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 48px;
  padding-inline: var(--space-5);
  background: #ffdd00;
  color: #111;
  border: 1px solid rgba(0, 0, 0, 0.22);
  border-radius: 999px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.22), 0 10px 30px rgba(0, 0, 0, 0.26);
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
  animation: bmc-in 420ms var(--ease) 600ms both;
  transition: background-color 160ms var(--ease), transform 160ms var(--ease),
              box-shadow 160ms var(--ease);
}
.bmc:hover {
  background: #ffe74d;
  color: #111;
  transform: translateY(-2px);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.24), 0 14px 38px rgba(0, 0, 0, 0.3);
}
.bmc:active { transform: translateY(0); }

@keyframes bmc-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (max-width: 640px) {
  .bmc {
    right: var(--space-4);
    bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    width: 52px;
    min-height: 52px;
    padding-inline: 0;
    justify-content: center;
  }
  /* Hidden visually, not removed: `display: none` would take the words out of
     the accessible name and leave the button announced as nothing at all. */
  .bmc__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ------------------------------------------------- the chapter rail ------ */

/*
  The page's table of contents, and the reason the header has none. Eight
  dashes down the right edge: the one you are in is accented and longest, and
  the rest fade with distance so the shape of the rail alone tells you roughly
  where in the page you are.

  The rail itself takes no pointer events -- only the links inside it do. A
  fixed strip down the side of a page must never be the reason a drag fails to
  scroll, and the panel behind the labels is the easiest way to get that wrong.
*/
.rail {
  position: fixed;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  z-index: 55;
  pointer-events: none;
}

.rail ul {
  position: relative;
  display: grid;
  gap: 2px;
  margin: 0;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-5);
  list-style: none;
}

/* The backdrop that appears under the labels, not under the resting dashes. */
.rail ul::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--card);
  background: color-mix(in srgb, var(--card) 84%, transparent);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  backdrop-filter: saturate(160%) blur(14px);
  border: 1px solid var(--border);
  border-right: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  box-shadow: var(--shadow-card);
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms var(--ease);
}

.rail__item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  height: 20px;
  color: var(--text-muted);
  text-decoration: none;
  pointer-events: auto;
}

.rail__label {
  font-size: 0.75rem;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  transform: translateX(6px);
  transition: opacity 180ms var(--ease), transform 180ms var(--ease),
              color 160ms var(--ease);
}

/*
  --dist is how many sections away this one is, written by script. Nearer means
  longer and brighter; that gradient is the entire signal, so it is worth it
  being a calculation rather than a handful of hard-coded states.
*/
.rail__dash {
  display: block;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  width: calc(22px - clamp(0px, var(--dist, 4) * 3px, 12px));
  opacity: clamp(0.16, calc(1 - var(--dist, 4) * 0.2), 1);
  transition: width 220ms var(--ease), opacity 220ms var(--ease),
              background-color 160ms var(--ease);
}

.rail__item[aria-current] .rail__dash {
  width: 26px;
  opacity: 1;
  background: var(--accent-icon);
}
.rail__item[aria-current] .rail__label { color: var(--text); font-weight: 600; }

.rail:hover ul::before,
.rail:focus-within ul::before,
.rail.is-open ul::before { opacity: 1; }

.rail:hover .rail__label,
.rail:focus-within .rail__label,
.rail.is-open .rail__label { opacity: 1; transform: none; }

.rail__item:hover .rail__dash,
.rail__item:focus-visible .rail__dash { background: var(--accent-icon); opacity: 1; }
.rail__item:hover .rail__label { color: var(--text); }

/*
  On a touchscreen there is no hover to give, so the first tap opens the rail
  and the second picks a section -- which is also what stops a thumb resting on
  the edge of the screen from throwing you to another part of the page.
  Taller rows, because 20px is not a tap target.
*/
@media (max-width: 820px) {
  /* Closer to the edge, where there is less page to crowd. */
  .rail ul { padding: var(--space-2) 6px var(--space-2) var(--space-4); gap: 0; }
  .rail__item { height: 38px; }
  .rail__dash { width: calc(16px - clamp(0px, var(--dist, 4) * 2px, 8px)); }
  .rail__item[aria-current] .rail__dash { width: 20px; }
}

/* Nothing to point at in landscape on a phone, and it would cover the words. */
@media (max-height: 420px) { .rail { display: none; } }

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

/*
  It sits in the header now that there is room there, as a bare icon: a puck
  floating over the page was one more thing competing with the coffee button
  for the same corner of your eye. The box is still 44px square even though
  nothing is drawn around it -- the target has to be big enough to hit whether
  or not there is a circle to aim at.
*/
.theme-toggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-left: auto;
  padding: 0;
  background: none;
  color: var(--text-muted);
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color 160ms var(--ease);
}
.theme-toggle:hover { color: var(--text); }

/* Only one of the two is ever drawn, and which one is a property of the
   palette -- so the button offers the theme you are not currently in. */
.icon--sun { display: var(--icon-sun); }
.icon--moon { display: var(--icon-moon); }

/*
  The theme change sweeps down the page instead of snapping. A View Transition
  snapshots the old palette and the new one, and revealing the new over the old
  from the top down is what turns a swap into something you can watch happen.
  Browsers without the API get the swap alone, which is not a downgrade worth
  apologising for.

  The first version wiped with a hard `clip-path` edge over half a second on an
  ease-out curve, and it read as a stutter for two reasons. A hard edge is a
  single row of pixels crossing a thousand of them, so every frame it misses is
  a jump you can measure by eye. And an ease-out spends most of its time in the
  last fraction of the distance, so the tail crawls just when there is least
  left to look at.

  So: a soft band instead of a line -- there is no longer an edge whose exact
  position can be read, which is also what hides a dropped frame -- and a curve
  that is nearly constant speed, over a shorter run.
*/
::view-transition-group(root),
::view-transition-image-pair(root),
::view-transition-old(root),
::view-transition-new(root) { animation: none; }

::view-transition-old(root) { z-index: 0; }

::view-transition-new(root) {
  z-index: 1;
  /*
     Three viewports tall, opaque for the first third and clear by two thirds,
     which puts the fading band exactly one screen deep. Those stops are what
     make the two ends of the sweep land on round numbers: at 100% the slice on
     screen is entirely past the clear stop, and at 0% it is entirely before the
     opaque one -- so the whole run does work and neither end idles. Measured
     rather than assumed; the alpha ramps 0, 32, 128, 223, 255 across it.
  */
  -webkit-mask-image: linear-gradient(to bottom, #000 0 33.4%, transparent 66.6%);
          mask-image: linear-gradient(to bottom, #000 0 33.4%, transparent 66.6%);
  -webkit-mask-size: 100% 300%;
          mask-size: 100% 300%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  animation: theme-sweep 480ms cubic-bezier(0.4, 0.1, 0.3, 1) both;
}

/* At 100% the visible slice of the mask is its transparent tail, so nothing of
   the new palette shows; at 0% the slice is the opaque head, so all of it does. */
@keyframes theme-sweep {
  from { -webkit-mask-position: 0 100%; mask-position: 0 100%; }
  to   { -webkit-mask-position: 0 0%;   mask-position: 0 0%; }
}

/* ----------------------------------------------------------- forced colours */

/*
  Windows High Contrast. The app itself honours the user's high-contrast scheme,
  so the page should not be the thing that ignores it. Everything below either
  hands a surface back to the system palette or gives a shape that was defined
  only by colour an actual border to be seen by.
*/
@media (forced-colors: active) {
  .site-header {
    background: Canvas;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .window,
  .xp,
  .mini,
  .format,
  .download,
  .notice,
  .keys,
  .faq,
  .fmt__panel,
  .privacy__item,
  .art-photo,
  .mini__art,
  .xp__thumb { border: 1px solid CanvasText; }

  /* A mask hides by making pixels transparent, which forced colours cannot
     compensate for -- so the keyboard shows in full instead of half-vanishing. */
  .kb {
    -webkit-mask-image: none;
            mask-image: none;
  }
  .kb__key { border-color: CanvasText; }
  .kb__key--space {
    border-color: Highlight;
    border-width: 2px;
    box-shadow: none;
    color: CanvasText;
  }

  /* Opacity-based greys vanish against a forced background; these are bars in
     an illustration, so draw them as real ones. */
  .mini__line,
  .mini__glyph,
  .mini__wave i,
  .mini__cardrows i {
    background: CanvasText;
    opacity: 1;
    forced-color-adjust: none;
  }

  .xp__tile--target::before,
  .mini__rows--nav::before {
    background: Highlight;
    forced-color-adjust: none;
  }

  .rail ul::before { background: Canvas; border-color: CanvasText; }
  .rail__dash {
    background: CanvasText;
    opacity: 1;
    forced-color-adjust: none;
  }
  .rail__item[aria-current] .rail__dash { background: Highlight; }

  .theme-toggle { color: ButtonText; }

  .bmc {
    background: ButtonFace;
    color: ButtonText;
    border: 2px solid ButtonText;
    box-shadow: none;
  }

  :where(a, button, [tabindex]):focus-visible { outline-color: Highlight; }
}

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

/*
  Someone who turned animation off asked for less movement, not for a broken
  page. Both illustrations are authored in their finished state, so neutralising
  every animation here leaves the window open and the spacebar lit -- which is
  the whole point of each drawing.
*/
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

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