/* onecatch.app — the whole stylesheet for both language pages.
   Shared rather than inlined per page: two copies of 700 lines drift, and a
   colour fixed on one page but not the other is exactly the bug that would
   follow. Loaded with a plain <link>, so there is still no build step. */

/* ====================================================================
   Brand tokens.
   Copied from the application's own palette — frontend/src/index.css,
   the `brand-tokens` block in the openmodu/onecatch repo. That file is
   the source of truth; this is a hand-kept copy so the site stays a
   single file with no build step. Re-copy the block when it changes.
   ==================================================================== */
:root {
  --radius: 0.5rem;
  --font-ui-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
  --font-ui-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
}

/* Light: the mirage cream canvas, with cards lifted above it. */
:root {
  --background: #f5f5f0;
  --foreground: #1a1a1a;
  --card: #fcfcfa;
  --primary: #694d1f;
  --primary-foreground: #f7f4ec;
  --muted-foreground: #60605e;
  --border: #d5d2c6;
  --success: #33732e;
  --warning: #a15e14;
  --info: #216e78;
  --sidebar: #efeee7;
}

/* Dark: charcoal canvas, amber stays the identity colour. Declared twice
   on purpose — once for "follow the OS", once for the pinned choice. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --background: #1c1c1c;
    --foreground: #efefef;
    --card: #232323;
    --primary: #edc278;
    --primary-foreground: #1f1a10;
    --muted-foreground: #ababab;
    --border: #3a3a3a;
    --success: #91e082;
    --warning: #ffba54;
    --info: #73e0eb;
    --sidebar: #202020;
  }
}

:root[data-theme="dark"] {
  --background: #1c1c1c;
  --foreground: #efefef;
  --card: #232323;
  --primary: #edc278;
  --primary-foreground: #1f1a10;
  --muted-foreground: #ababab;
  --border: #3a3a3a;
  --success: #91e082;
  --warning: #ffba54;
  --info: #73e0eb;
  --sidebar: #202020;
}

/* ====================================================================
   Site roles. Every value resolves through the palette above, so a
   colour change lands here in both themes without a second copy.
   ==================================================================== */
:root {
  --ground: var(--background);
  --panel: var(--card);
  --band: var(--sidebar);
  --ink: var(--foreground);
  --ink-soft: var(--muted-foreground);
  --rule: var(--border);
  /* The plain border first: Safari before 16.4 — iOS 16.3 and older — has no
     color-mix(), and an unresolvable var() in a border-color computes to
     currentColor, which turns every soft hairline on the page into full ink. */
  --rule-soft: var(--border);
  --rule-soft: color-mix(in oklab, var(--border) 55%, var(--background));
  --amber: var(--primary);
  --amber-ink: var(--primary-foreground);
  --ok: var(--success);
  --warn: var(--warning);
  --sans: var(--font-ui-sans);
  --mono: var(--font-ui-mono);

  --gutter: 34px;
  --column: 920px;
}

*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* iOS drops a grey plate over anything tapped, screenshots included. */
  -webkit-tap-highlight-color: rgba(148, 110, 40, 0.16);
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.8;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }

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

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: 10px 16px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 8px;
  font-size: 14px;
  text-decoration: none;
}
.skip:focus { left: 12px; top: 12px; z-index: 10; }

/* ---- The spine ---------------------------------------------------
   One hairline runs the full length of the page along the left edge of
   the reading column, and every section hangs a tick on it. The page is
   read the way a run log is read: top to bottom, one step at a time. */
.column {
  position: relative;
  max-width: var(--column);
  margin: 0 auto;
  padding: 0 24px 0 calc(24px + var(--gutter));
}

.column::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 24px;
  width: 1px;
  background: var(--rule);
}

/* The tick hangs off the eyebrow, not off the section box — anchored to
   the section's padding it floats a whole gutter above its words. */
.eyebrow::before {
  content: "";
  position: absolute;
  top: 0.5em;
  left: calc(-1 * var(--gutter));
  width: 13px;
  height: 2px;
  background: var(--amber);
}

/* ---- Header ------------------------------------------------------ */
.masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-top: 26px;
  padding-bottom: 26px;
}

.mark {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
}

/* The application's actual icon, not an approximation of it. The file already
   carries the rounded square and its own transparent corners, so the page adds
   no plate and no radius — anything it added would be a second, slightly wrong
   version of the mark. */
.glyph {
  display: block;
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
}

/* The name does not explain itself, so the promise rides with it — set
   apart from the wordmark by a rule rather than by shouting. */
.tagline {
  padding-left: 11px;
  margin-left: 3px;
  border-left: 1px solid var(--rule);
  color: var(--ink-soft);
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0.01em;
}

.masthead-links {
  display: flex;
  align-items: center;
  gap: 22px;
  font-size: 14px;
  color: var(--ink-soft);
}

.masthead-links a { text-decoration: none; }
.masthead-links a:hover { color: var(--ink); }

/* Language switch. Both languages are visible and both are real links, so the
   reader can see which one they are on and either is one tap away — a single
   toggling link hides the current state and is a guess about where it leads.
   The glyphs are each written in their own language: a reader who cannot read
   this page still recognises their own. */
.lang {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 28px;
  padding: 2px;
  border: 1px solid var(--rule);
  border-radius: 8px;
}

.lang a {
  display: grid;
  place-items: center;
  height: 22px;
  min-width: 26px;
  padding: 0 6px;
  border-radius: 6px;
  color: var(--ink-soft);
  font-size: 12px;
  line-height: 1;
  text-decoration: none;
}

.lang a:hover { color: var(--ink); }

.lang a[aria-current] {
  background: var(--amber);
  color: var(--amber-ink);
  font-weight: 500;
}

.theme-toggle {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
}

.theme-toggle:hover { border-color: var(--ink-soft); color: var(--ink); }
.theme-toggle .sun { display: none; }
.theme-toggle .moon { display: block; }

/* The icon shows the theme you would switch *to*, so it has to track the
   resolved theme through all three states, not just the stamped ones. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .sun { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .moon { display: none; }
}

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

/* ---- Type scale --------------------------------------------------- */
h1 {
  margin: 0;
  font-size: clamp(2.35rem, 5.4vw, 3.4rem);
  font-weight: 600;
  line-height: 1.14;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

h2 {
  margin: 0 0 10px;
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

.feature h3 {
  margin: 0 0 6px;
  font-size: 1.1875rem;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: -0.015em;
}

p { margin: 0; }

.eyebrow {
  position: relative;
  margin: 0 0 14px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.lede {
      color: var(--ink-soft);
  font-size: 1.0625rem;
}

code {
  font-family: var(--mono);
  font-size: 0.86em;
  color: var(--ink);
}

/* ---- Hero --------------------------------------------------------- */
.hero { padding: clamp(56px, 11vh, 104px) 0 clamp(48px, 9vh, 88px); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 26px;
  padding: 5px 12px 5px 10px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--panel);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-soft);
}

.chip code { font-size: 12px; color: var(--ink-soft); }

.chip .dot {
  width: 5px;
  height: 5px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--ok);
}

.hero .lede { margin-top: 22px; }

.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 32px;
}

.button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 38px;
  padding: 0 18px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  transition: background-color 130ms ease, border-color 130ms ease;
}

.button.primary { background: var(--amber); color: var(--amber-ink); }
.button.primary:hover { background: color-mix(in oklab, var(--amber) 86%, var(--ink)); }
.button.ghost { border-color: var(--rule); color: var(--ink); }
.button.ghost:hover { border-color: var(--ink-soft); }

.under-cta {
  margin-top: 14px;
  font-size: 13px;
  color: var(--ink-soft);
}

.under-cta a { text-decoration: underline; text-underline-offset: 3px; }
.under-cta a:hover { color: var(--ink); }

.drives {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 18px;
  margin-top: 40px;
  font-size: 13px;
  color: var(--ink-soft);
}

.drives .label { letter-spacing: 0.1em; text-transform: uppercase; font-size: 11.5px; }

.drives .cli {
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--ink);
}

/* ---- Transcript band ----------------------------------------------
   The one element that breaks the column. Not a screenshot: the
   application's real normalized event stream, which is the thing the
   product actually sells. */
.band {
  border-top: 1px solid var(--rule-soft);
  border-bottom: 1px solid var(--rule-soft);
  background: var(--band);
}

/* The padding belongs to the inner column, not the band: that is what
   lets the spine run the full height of the band rather than stopping at
   its content and leaving a gap at each end. */
.band > .column {
  padding-top: clamp(44px, 7vh, 72px);
  padding-bottom: clamp(44px, 7vh, 72px);
}

.terminal {
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--panel);
  box-shadow: 0 18px 44px -30px color-mix(in oklab, var(--ink) 55%, transparent);
}

.terminal-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--rule-soft);
}

.terminal-bar .light {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--rule);
}

.terminal-title {
  margin-left: 6px;
  font-size: 12.5px;
  color: var(--ink-soft);
}

.terminal-body {
  padding: 16px 14px 18px;
  overflow-x: auto;
}

.stream {
  margin: 0;
  min-height: 21.5em;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.85;
  white-space: pre;
}

.stream .row { display: block; }
.stream .kind { display: inline-block; width: 13ch; }
.stream .started { color: var(--info); }
.stream .reasoning, .stream .tool_result, .stream .usage { color: var(--ink-soft); }
.stream .tool_use { color: var(--ink); }
.stream .file_change { color: var(--amber); }
.stream .result { color: var(--ok); }
.stream .result-warn { color: var(--warn); }
.stream .divider { color: var(--rule); }
.stream .text { color: var(--ink-soft); }
.stream .tool_use + .text, .stream .file_change + .text { color: var(--ink); }

.caret {
  display: inline-block;
  width: 7px;
  height: 1em;
  background: var(--amber);
  vertical-align: -2px;
}

.caret.blink { animation: caret 1.05s steps(2, start) infinite; }

@keyframes caret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

/* ---- Sections ------------------------------------------------------ */
.section { padding: clamp(52px, 9vh, 86px) 0 0; }

.features {
  display: flex;
  flex-direction: column;
  gap: 44px;
  margin-top: 38px;
}

.feature p { color: var(--ink-soft); }

.steps {
  display: flex;
  flex-direction: column;
  gap: 26px;
  margin-top: 34px;
}

.steps p { color: var(--ink-soft); }
.steps strong { color: var(--ink); font-weight: 600; }

/* Scoped to .code on purpose: a bare `pre` rule here also captures the
   transcript, which is a `pre` too, and boxes it in its own window. */
.code {
  margin: 22px 0 0;
  padding: 16px 18px;
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: var(--panel);
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.9;
}

.code .prompt { color: var(--ink-soft); user-select: none; }
.code .note { color: var(--ink-soft); }

.note-line {
  margin-top: 18px;
  color: var(--ink-soft);
  font-size: 0.9375rem;
}

/* ---- Download ------------------------------------------------------ */
.platforms {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-top: 34px;
}

/* A card is no longer a single link: Linux ships two packages, and one card
   cannot lead to two places. So the card is a container and every download is
   its own explicit link — which also means the size is attached to the thing
   it describes. 16 MB and 100 MB are not the same decision. */
.platform {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px 16px 14px;
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: var(--panel);
}

.platform .os {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 600;
}

.platform .os svg { flex: 0 0 auto; color: var(--ink-soft); }
.platform .detail { color: var(--ink-soft); font-size: 12.5px; line-height: 1.6; }

/* The one thing a download card is for, so it gets the full width of the card
   and the same height in every one — as pills sized to their own labels, the
   row read as mismatched shapes, and ".exe" in particular came out as the
   smallest, faintest element in a card whose only job is to be downloaded from.
   One per row rather than side by side: every label is "<what it is for>
   <extension>", and two of those do not fit across a 183px card. */
.gets {
  display: grid;
  gap: 8px;
  margin-top: auto;
  padding-top: 14px;
}

.gets a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: 38px;
  padding: 0 10px;
  border: 1px solid var(--rule);
  border-radius: 10px;
  /* The page ground, not the card's. A plate a shade darker than the card it
     sits in reads as a control at rest, without a fill loud enough to argue
     with the hero button. */
  background: var(--ground);
  color: var(--amber);
  font-size: 13.5px;
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
  transition: border-color 130ms ease, background-color 130ms ease;
}

/* Not a solid amber fill: the hero has the page's one solid button, and three
   more here would argue with it. The tint is enough to say which one is under
   the cursor. */
.gets a:hover {
  border-color: var(--amber);
  background: color-mix(in oklab, var(--amber) 10%, var(--ground));
}

.gets a svg { flex: 0 0 auto; opacity: 0.75; }

/* The version, wherever it appears. Mono because it is a machine's word, and
   boxed so it reads as a fact about the build rather than as part of the
   sentence around it. Every instance carries data-release-version, so
   sync-release.py updates all of them at once. */
.version {
  display: inline-block;
  margin-right: 4px;
  padding: 1px 7px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--ink-soft);
  white-space: nowrap;
}

.footer .version { margin-right: 0; }

.release-line {
  margin-top: 18px;
  color: var(--ink-soft);
  font-size: 13px;
  line-height: 1.7;
}

.release-line a { text-decoration: underline; text-underline-offset: 3px; }
.release-line a:hover { color: var(--ink); }
.release-line code { font-size: 11.5px; color: var(--ink-soft); }

.requires {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 28px;
  padding: 16px 18px;
  border: 1px solid var(--rule-soft);
  border-radius: 12px;
  font-size: 0.9375rem;
  color: var(--ink-soft);
}

.requires strong { color: var(--ink); font-weight: 600; }

/* The workbench used to be redrawn here in DOM from the application's tokens.
   It is a real screenshot now, so those ~150 lines are gone — including a
   `min-width: 620px` that would have forced the page wider than a phone. */

/* ---- Harness strip -------------------------------------------------
   The runtimes' own marks, at the size they appear in the application's
   own menus. assets/runtime/SOURCES.md records where each comes from. */
.harnesses { margin-top: 40px; }

.harnesses .label {
  margin-bottom: 12px;
  color: var(--ink-soft);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.harnesses ul {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.harnesses li {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 12px 5px 9px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--panel);
  font-size: 13px;
}

/* The marks are drawn for a light ground; in dark they would otherwise
   sit on nothing. A neutral plate keeps every one of them legible
   without recolouring anyone's logo. */
.harnesses img,
.harnesses .mark {
  display: block;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  object-fit: contain;
}

/* Codex, Claude and the rest carry their own brand colours and are fine
   either way. The Modu mark is a black raster silhouette, so on a dark
   chip it needs flipping — the currentColor marks above do not, which is
   why they are inlined rather than loaded as images. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .harnesses img[src$="modu-code.png"] { filter: invert(1); }
}

:root[data-theme="dark"] .harnesses img[src$="modu-code.png"] { filter: invert(1); }

/* ---- Figures ---------------------------------------------------------
   Two kinds, because the screenshots are two kinds. A `window` shot is a
   whole application window captured with its own shadow and rounded
   corners, so the page must add nothing; an `inset` shot is a crop from
   inside the interface, which needs a border to read as a picture rather
   than as part of the page. */
/* The spine is an absolutely positioned pseudo-element, so it paints
   above static content by default and draws a hairline straight across a
   breakout screenshot. Lifting the figure onto its own layer puts the
   picture back on top of the rail it crosses. */
.figure {
  position: relative;
  z-index: 1;
  margin: 34px 0 0;
}

.figure img {
  display: block;
  width: 100%;
  height: auto;
}

/* At the prose measure a screenshot is readable; at 334px on a phone it is
   not, and no amount of cropping fixes that. So each one links to itself —
   tapping opens the full-resolution file, which is the only way to actually
   read the interface on a small screen. */
.figure-zoom {
  display: block;
  position: relative;
  text-decoration: none;
}

/* The hint is only ever shown on a phone (see the narrow-screen block), but
   its wording has to come from somewhere both pages share. :lang() reads the
   <html lang>, so each page gets its own without a second stylesheet. */
:root { --zoom-hint: "Tap to enlarge"; }
:root:lang(zh-Hans) { --zoom-hint: "\70b9\5f00\770b\5927\56fe"; }

/* No radius. An inset shot is a rectangular slice cut out of the interface, and
   the interface inside it already has rounded corners of its own — wrapping a
   second radius around that invents a card the screenshot is not, and leaves
   two curves nested with a sliver of the app's background between them. On a
   dark page, where the bright crop meets the ground at full contrast, that
   outer curve is the first thing you see. The border alone is enough to say
   "this is a picture". */
.figure.inset img {
  border: 1px solid var(--rule);
}

/* The capture already carries a drop shadow in its transparent margin,
   so a second one would double up. */
.figure.window img { border-radius: 0; }


/* The captures are of the application's light theme. On a dark page a
   full-brightness screenshot glares, so it is eased down a little and
   given an edge to sit against. Swap in dark-theme captures with
   <picture> and this can go. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .figure img { filter: brightness(0.92); }
  :root:not([data-theme="light"]) .figure.inset img { border-color: var(--rule); }
}

:root[data-theme="dark"] .figure img { filter: brightness(0.92); }

.figure figcaption {
  margin-top: 14px;
  color: var(--ink-soft);
  font-size: 12.5px;
  line-height: 1.6;
}

.figure figcaption em { font-style: normal; color: var(--ink); }

/* The first figure in a band already has the band's padding above it, so
   it drops the section margin. Later ones still need the gap — without
   this they collide with the caption above them. */
.band .figure:first-child { margin-top: 0; }
.figure + .figure { margin-top: 40px; }

.section .lede { margin-bottom: 4px; }

/* ---- Questions ------------------------------------------------------ */
.qa { margin-top: 30px; border-top: 1px solid var(--rule-soft); }
.qa details { border-bottom: 1px solid var(--rule-soft); }

.qa summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 17px 0;
  font-size: 1rem;
  font-weight: 500;
  list-style: none;
  cursor: pointer;
}

.qa summary::-webkit-details-marker { display: none; }

.qa summary::after {
  content: "+";
  flex: 0 0 auto;
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 15px;
}

.qa details[open] summary::after { content: "–"; }
.qa summary:hover { color: var(--amber); }
.qa p { padding: 0 0 18px; color: var(--ink-soft); }

/* ---- Footer --------------------------------------------------------- */
.footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: clamp(64px, 11vh, 108px);
  padding: 26px 0 40px;
  border-top: 1px solid var(--rule-soft);
  font-size: 13.5px;
  color: var(--ink-soft);
}

.footer a { text-decoration: none; }
.footer a:hover { color: var(--ink); }
.footer .path { font-family: var(--mono); font-size: 12.5px; }
.footer-links { display: flex; gap: 16px; }

/* ---- Narrow screens ------------------------------------------------------
   The masthead is the one row that cannot simply reflow: it is a flex row that
   does not wrap, so on a phone it set the page's scroll width and every other
   element laid itself out against that instead of the viewport. Hence both the
   wrap and the two links that step aside — Features and FAQ point at sections
   the reader reaches by scrolling anyway, while Download and GitHub are the
   two that actually leave the page. */
@media (max-width: 860px) {
  .masthead { flex-wrap: wrap; }
  .masthead-links { flex-wrap: wrap; }
}

@media (max-width: 640px) {
  :root { --gutter: 20px; }
  body { font-size: 16px; }
  .platforms { grid-template-columns: minmax(0, 1fr); }
  /* No room for the promise once the nav is there. */
  .tagline { display: none; }
  .nav-secondary { display: none; }
  .masthead-links { gap: 13px; }
  .column { padding-right: 18px; padding-left: calc(18px + var(--gutter)); }
  .column::before { left: 18px; }

  h1 { font-size: clamp(2rem, 8.5vw, 2.6rem); }
  h2 { font-size: 1.5rem; }

  /* A phone has no room for two buttons side by side at this size, and a
     half-visible second button reads as breakage rather than as a choice. */
  .actions { gap: 10px; }
  .button { height: 40px; }

  /* ---- Touch ----------------------------------------------------------
     Two things read as breakage on a phone rather than as a small screen. */

  /* 1. The header wraps to two rows here, and the hero's own top padding then
     opens a second empty screen under it. Both ends give ground. */
  .masthead { padding-top: 16px; padding-bottom: 14px; }
  .hero { padding-top: 32px; }

  /* 2. iOS shows no scrollbar until you are already scrolling, so a box that
     scrolls sideways just looks like text cut off mid-word. The transcript
     stops being a fixed grid and becomes a label column with the message
     wrapping beside it; the build commands wrap and put their comment on its
     own line. Neither one is clipped any more, so neither needs a scrollbar
     to explain itself. */
  .terminal-body { overflow-x: hidden; }
  .stream { font-size: 11.5px; line-height: 1.7; white-space: pre-wrap; min-height: 30em; }
  .stream .row { display: flex; gap: 7px; }
  .stream .kind { width: auto; flex: 0 0 11ch; }
  .stream .text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
  .stream .divider { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; }

  /* pre-line rather than pre-wrap: the runs of spaces that align the trailing
     comments at the prose measure are noise once the line has to wrap, and
     collapsing them is what keeps a wrapped command on two lines instead of
     four. The newlines that matter are still newlines. */
  .code { font-size: 12px; overflow-x: visible; white-space: pre-line; overflow-wrap: break-word; }

  /* A screenshot at this width is a thumbnail. Each one has always opened
     full-size on tap — but on a phone there is no cursor to change shape, so
     until now nothing said so. */
  .figure-zoom::after {
    content: var(--zoom-hint);
    position: absolute;
    right: 8px;
    bottom: 8px;
    padding: 2px 8px;
    border-radius: 999px;
    /* Fixed rather than themed: it sits on a screenshot, not on the page. */
    background: rgba(20, 20, 20, 0.6);
    color: #fff;
    font-size: 10px;
    line-height: 1.5;
    letter-spacing: 0.01em;
    /* The badge is inside the link, so a tap that lands on it still opens the
       image; taking it out of hit-testing only keeps it from eating the tap. */
    pointer-events: none;
  }
}

/* The narrowest phones still in use. Nothing here is a new idea, only less of
   everything. */
@media (max-width: 400px) {
  .masthead-links { gap: 12px; font-size: 13px; }
  .actions .button { width: 100%; justify-content: center; }
  .chip { flex-wrap: wrap; row-gap: 2px; font-size: 11.5px; }
}

/* ---- Touch targets -------------------------------------------------------
   Keyed to the pointer rather than to the width: an iPhone held sideways is
   844 CSS px wide and still has nothing but a fingertip on it. Apple's floor
   is 44pt, and the language switch — the control this page's readers use
   most — was the smallest thing on the page at 26x22. The nav links take
   their height from padding, so the hit box grows without the text moving. */
@media (pointer: coarse) {
  .masthead-links > a { padding: 10px 0; }
  .lang { height: 38px; }
  .lang a { height: 32px; min-width: 40px; font-size: 13px; }
  .theme-toggle { width: 34px; height: 34px; }
  .gets a { height: 44px; }
  .footer-links a, .footer .path { padding: 6px 0; }
  /* Nothing here has a hover state to show, and iOS keeps the last hovered
     element styled until you tap elsewhere. */
  .button, .masthead-links a, .lang a, .gets a { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .caret.blink { animation: none; }
  .button { transition: none; }
}
