/* Keysi marketing site — one stylesheet, no build step.
   Dark/light via prefers-color-scheme only; no JS theme toggle by design
   (respecting system appearance is the Mac-native behavior). */

/* ---- Type ----------------------------------------------------------
   Three roles, and which face plays each one is a decision rather than a
   default.

   Two self-hosted variable woff2 files carry personality where it shows:
   Schibsted Grotesk for display, Martian Mono for the small mechanical
   labels. Everything that is *read* — body copy, UI, keycaps, code —
   stays on the system stack, which on the Mac this app targets resolves
   to SF Pro and SF Mono. That is the native reading experience, costs
   nothing to load, and is the right answer for keycaps specifically: the
   Mac modifier glyphs (⌘ ⇧ ⌥ ⌃) live in Miscellaneous Technical, which
   the latin subsets below do not cover, so a webfont keycap would fall
   back for exactly the characters that matter most.

   Self-hosted and not fetched from Google: a gstatic request would hand
   every visitor's IP to a third party before the consent banner is even
   answered, which would quietly contradict privacy.html. See
   assets/fonts/README.md. */
/* `unicode-range` is not an optimisation here, it is correctness. These are
   latin subsets, and the site is getting localised into nine languages —
   three of which (ja, ko, zh-Hans) are mostly outside that range. Without a
   declared range the browser still selects these faces for a CJK heading and
   then falls back per glyph. Declaring the range means the browser never
   picks them for CJK text, and a CJK page stops downloading fonts it cannot
   use. It does not stop the Latin *inside* a CJK heading using them, which
   still mixes two faces; the `:lang()` override after `:root` below does
   that, by keeping CJK pages' headings and labels on the system stack. */
@font-face {
  font-family: "Schibsted Grotesk";
  src: url("../fonts/schibsted-grotesk-var-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC,
    U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Martian Mono";
  src: url("../fonts/martian-mono-var-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC,
    U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light dark;

  /* ---- Light palette (default) ----
     The page is an aluminium deck and the keycaps are the lit things on it.
     This used to be a warm cream (#f7f6f3), which put a warm terracotta
     accent on a warm ground: the same temperature everywhere, so the accent
     had nothing to push against and the one button that must win didn't.
     Anodised silver is also simply what the hardware this app runs on is
     made of. Cool neutrals, one warm accent, and the keys brighter than
     everything around them. */
  --bg: #e9eaec;
  --bg-elevated: #f7f8f9;
  --bg-sunken: #dcdee1;
  --text: #16181b;
  --text-secondary: #4d5257;
  --text-tertiary: #5d6369;
  --border: #cfd2d6;
  --border-strong: #b4b8bd;

  --accent: #a6532c;
  --accent-strong: #8a4322;
  --accent-text: #fff8f0;
  --accent-tint: #f3e3d6;
  --link: #8a4322;
  --link-hover: #6d3418;

  /* Keycap material. Four tokens rather than one, because a key that reads
     as a real object needs a face, a side wall you can see under it, an
     edge, and its own shadow — the old single flat gradient was a chip. */
  --keycap-bg: linear-gradient(180deg, #ffffff 0%, #eef0f2 100%);
  --keycap-border: #c2c6cb;
  --keycap-text: #202327;
  --keycap-wall: #ccd0d4;
  --keycap-gloss: rgba(255, 255, 255, 0.95);
  --keycap-ambient: rgba(30, 25, 15, 0.22);

  --warn-bg: #fbeee0;
  --warn-border: #e7b781;
  --warn-text: #7a4a10;

  --good: #3f6b3a;

  --shadow-sm: 0 1px 2px rgba(20, 16, 8, 0.06);
  --shadow-md: 0 8px 24px rgba(20, 16, 8, 0.10);
  --shadow-lg: 0 24px 64px rgba(20, 16, 8, 0.16);

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

  /* The horizontal gutter every page keeps between its content and the
     screen edge. Only visible below ~1128px, where .container's max-width
     stops doing the job and this padding is the only thing holding the
     content off the bezel. */
  --gutter: 24px;

  /* Vertical rhythm is the only hierarchy a one-column page has, so it gets
     a scale rather than one value. Every section used to take a flat 72px,
     which gave a 4,500px block and a 550px one the same separation and made
     scrolling tell you nothing about where you were. These are per-side, so
     adjacent sections add up to twice the number: 112px between major beats,
     64px between sections that belong to each other. */
  --space-major: 56px;
  --space-minor: 32px;

  /* One cap for running text, applied to every prose block rather than only
     .lede. The trust list used to run 1,032px — about 129 characters — at
     the 1080px container width. */
  --measure: 64ch;

  /* Read: the system, i.e. SF Pro on the platform this app runs on. */
  --font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  /* Heard: the voice of the page. */
  --font-display: "Schibsted Grotesk", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
  /* Machine: eyebrows, labels, tabular figures. Small and tracked out. */
  --font-label: "Martian Mono", "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  /* Literal: keycaps and code, which must look like the OS and the shell. */
  --font-mono: "SF Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* CJK pages: headings and labels on the system stack. The webfonts are
   latin-only, so `unicode-range` (above) keeps them off CJK glyphs — but the
   Latin in a mixed heading ("Mac 版 Vim の…") would still pick them up and
   render in a second face next to the system's Japanese one. One face per
   heading beats a display face on half of it. `:lang()` matches the
   generated pages' `<html lang>`, including zh-Hans. */
:root:lang(ja),
:root:lang(ko),
:root:lang(zh) {
  --font-display: -apple-system, BlinkMacSystemFont, "Hiragino Sans", "Hiragino Kaku Gothic ProN", "PingFang SC", "Apple SD Gothic Neo", "Helvetica Neue", Arial, sans-serif;
  --font-label: var(--font-display);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Space grey, the other finish the same hardware comes in. Not a
       near-black "dark mode" ground: a deck, one step lighter, so the keys
       and the accent both have something to sit on. */
    --bg: #1b1d20;
    --bg-elevated: #24272b;
    --bg-sunken: #141618;
    --text: #eceef0;
    --text-secondary: #b0b5ba;
    --text-tertiary: #8b9198;

    --border: #33373b;
    --border-strong: #474c51;

    --accent: #e08a52;
    --accent-strong: #f0a374;
    --accent-text: #1b1108;
    --accent-tint: #3a2416;
    --link: #f0a374;
    --link-hover: #f8c6a3;

    --keycap-bg: linear-gradient(180deg, #545a61 0%, #3a3f45 100%);
    --keycap-border: #697077;
    --keycap-text: #f7f9fa;
    --keycap-wall: #17191c;
    --keycap-gloss: rgba(255, 255, 255, 0.18);
    --keycap-ambient: rgba(0, 0, 0, 0.7);

    --warn-bg: #3a2b12;
    --warn-border: #7a5220;
    --warn-text: #e8b676;

    --good: #7cb377;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 24px 64px rgba(0, 0, 0, 0.55);
  }
}

:root[data-theme="dark"] {
  /* Space grey, the other finish the same hardware comes in. Not a
     near-black "dark mode" ground: a deck, one step lighter, so the keys
     and the accent both have something to sit on. */
  --bg: #1b1d20;
  --bg-elevated: #24272b;
  --bg-sunken: #141618;
  --text: #eceef0;
  --text-secondary: #b0b5ba;
  --text-tertiary: #8b9198;

  --border: #33373b;
  --border-strong: #474c51;

  --accent: #e08a52;
  --accent-strong: #f0a374;
  --accent-text: #1b1108;
  --accent-tint: #3a2416;
  --link: #f0a374;
  --link-hover: #f8c6a3;

  --keycap-bg: linear-gradient(180deg, #545a61 0%, #3a3f45 100%);
  --keycap-border: #697077;
  --keycap-text: #f7f9fa;
  --keycap-wall: #17191c;
  --keycap-gloss: rgba(255, 255, 255, 0.18);
  --keycap-ambient: rgba(0, 0, 0, 0.7);

  --warn-bg: #3a2b12;
  --warn-border: #7a5220;
  --warn-text: #e8b676;

  --good: #7cb377;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 24px 64px rgba(0, 0, 0, 0.55);
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

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

/* Keyboard focus, once, for everything that takes it. There were two
   ad-hoc rules before this and nothing else — so links and, worse, the
   primary button fell back to the browser default ring, which is close to
   invisible on terracotta. `--text` inverts with the theme, so the ring
   stays high-contrast on every surface this site has. */
:where(a, button, summary, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
  border-radius: 3px;
}

a { color: var(--link); text-decoration-color: color-mix(in srgb, var(--link) 45%, transparent); text-underline-offset: 2px; }
a:hover { color: var(--link-hover); text-decoration-color: currentColor; }

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 700;
  /* Tighter than the old -0.01em: Schibsted Grotesk sets wider than SF Pro
     at the same size, and display type wants the tracking closed up. */
  letter-spacing: -0.028em;
  text-wrap: balance;
  margin: 0 0 0.5em;
}
h1 { font-size: clamp(2.2rem, 5.2vw, 3.6rem); line-height: 1.02; }
h2 { font-size: clamp(1.55rem, 3.4vw, 2.15rem); line-height: 1.14; }
h3 { font-size: 1.15rem; line-height: 1.28; letter-spacing: -0.018em; }
p { margin: 0 0 1em; color: var(--text); }

/* Inline padding (gutter) and block padding (vertical rhythm) are kept on
   separate longhands on purpose. Several elements carry both a container
   class and a section class at once, and the `padding` shorthand made the
   higher-specificity rule wipe out the other axis — `section.tight`
   (0,1,1) beat `.container` (0,1,0) and dropped the gutter to 0, so the
   pricing cards, comparison table and FAQ sat flush against the screen
   edge on any viewport narrower than the 1080px max-width. Longhands on
   two different axes cannot collide no matter which rule wins. */
.container { max-width: 1080px; margin: 0 auto; padding-inline: var(--gutter); }
.container-narrow { max-width: 760px; margin: 0 auto; padding-inline: var(--gutter); }

section { padding-block: var(--space-major); }
section.tight { padding-block: var(--space-minor); }

.eyebrow {
  display: inline-block;
  font-family: var(--font-label);
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: 12px;
}

/* Which tier a feature belongs to, stated on the feature itself rather than
   only in the pricing table. Someone scrolling the features should never have
   to guess whether the thing they just got interested in costs money. */
.tier {
  display: inline-block;
  font-family: var(--font-label);
  font-size: 0.6rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 999px;
  margin-left: 10px;
  vertical-align: 1px;
  border: 1px solid transparent;
}
/* Both chips are neutral outlines. They used to be green and terracotta,
   which put the accent on every Pro feature's eyebrow, every tier chip, the
   price figure, the brand mark, all links and the Download button at once —
   six jobs, so the one place it has to win didn't. The accent now means
   "action": the primary button, and links. A chip is a label, not an
   action, and reads perfectly well as weight and case alone. */
.tier-free {
  color: var(--text-secondary);
  border-color: var(--border-strong);
  background: var(--bg-sunken);
}
.tier-pro {
  color: var(--text);
  border-color: var(--text-tertiary);
  background: var(--bg-sunken);
  font-weight: 600;
}

.lede {
  font-size: 1.15rem;
  color: var(--text-secondary);
  max-width: var(--measure);
}

/* A footnote qualifying the claim just above it — the dated "KeyCue's own
   site doesn't say this" caveat, the "everything above this is free" scope
   note, the Coach blind spots. It has to read as smaller print or it reads
   as more of the pitch.
   Base rule as of 2026-09-11: this used to exist only as `.story .fine` and
   `footer .fine`, so four of the five `.fine` paragraphs then on the site
   were rendering at full body weight — including two on the homepage, where
   the caveat sits inside `.feature-copy` rather than `.story`, and the only
   correctly styled one was the single instance inside `.story`. The whole point
   of those paragraphs is to visibly de-escalate a claim, so losing the
   styling quietly inverted their job. The two context rules below now only
   add what actually differs (spacing, and the footer's smaller size). */
.fine {
  font-size: 0.9rem;
  color: var(--text-tertiary);
}

/* ---- Nav ---- */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border-bottom: 1px solid var(--border);
}
.site-nav .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 60px;
}
.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--text);
  text-decoration: none;
}
.brand:hover { color: var(--text); }
.brand-mark {
  width: 26px; height: 26px;
  border-radius: 7px;
  background: linear-gradient(160deg, var(--accent), var(--accent-strong));
  color: var(--accent-text);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.95rem;
  flex-shrink: 0;
}
.nav-links {
  display: flex;
  align-items: center;
  gap: 22px;
  list-style: none;
  margin: 0; padding: 0;
}
.nav-links a { color: var(--text-secondary); font-size: 0.92rem; text-decoration: none; }
.nav-links a:hover { color: var(--text); }
.nav-links { display: none; }
@media (min-width: 720px) { .nav-links { display: flex; } }

/* ---- Mobile navigation ----
   Below 720px .nav-links is hidden, and for a long time nothing replaced it:
   the entire nav on a phone was the wordmark and one button, so Features,
   Pricing, FAQ and Changelog were unreachable from the top of the page.
   A <details> disclosure needs no JavaScript and no build step — it fits
   this repo's no-toolchain rule — and it is keyboard-operable and
   screen-reader-labelled for free. */
.nav-more { position: relative; }
@media (min-width: 720px) { .nav-more { display: none; } }
.nav-more > summary {
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px; height: 34px;
  margin-left: 4px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  color: var(--text);
  cursor: pointer;
}
.nav-more > summary::-webkit-details-marker { display: none; }
.nav-more-glyph { position: relative; display: block; width: 15px; height: 1.5px; background: currentColor; border-radius: 1px; }
.nav-more-glyph::before, .nav-more-glyph::after {
  content: ""; position: absolute; left: 0;
  width: 15px; height: 1.5px; background: currentColor; border-radius: 1px;
}
.nav-more-glyph::before { top: -5px; }
.nav-more-glyph::after  { top: 5px; }
.nav-more[open] .nav-more-glyph { background: transparent; }
.nav-more[open] .nav-more-glyph::before { top: 0; transform: rotate(45deg); }
.nav-more[open] .nav-more-glyph::after  { top: 0; transform: rotate(-45deg); }

.nav-more-menu {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 30;
  min-width: 184px;
  margin: 0; padding: 6px;
  list-style: none;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}
.nav-more-menu a {
  display: block;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 0.95rem;
  text-decoration: none;
}
.nav-more-menu a:hover { background: var(--bg-sunken); }

/* Visually hidden, still announced. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 20px;
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
  font-weight: 600;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  font-family: var(--font-ui);
  transition: transform 0.08s ease, box-shadow 0.15s ease;
  white-space: nowrap;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--accent); color: var(--accent-text); box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--accent-strong); color: var(--accent-text); box-shadow: var(--shadow-md); }
.btn-secondary { background: var(--bg-elevated); color: var(--text); border-color: var(--border-strong); }
.btn-secondary:hover { border-color: var(--text-tertiary); color: var(--text); }
.btn-nav { padding: 8px 16px; font-size: 0.88rem; }
.btn-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.btn-lg { padding: 14px 26px; font-size: 1.02rem; }
.btn-sm { padding: 8px 14px; font-size: 0.86rem; }

/* ---- Consent banner (assets/js/analytics.js) — shown once, at most,
   until the visitor accepts or declines. Fixed to the viewport bottom
   rather than inline so it never shifts page layout on arrival. ---- */
.consent-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 100;
  display: flex;
  flex-wrap: wrap;
  gap: 12px 20px;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}
.consent-banner-text {
  margin: 0;
  flex: 1 1 320px;
  font-size: 0.88rem;
  color: var(--text-secondary);
}
.consent-banner-text a { color: var(--accent); }
.consent-banner-actions {
  display: flex;
  gap: 10px;
  flex: 0 0 auto;
}
@media (min-width: 640px) {
  .consent-banner { left: auto; max-width: 420px; }
}

/* ---- Keycap component — the recurring visual motif ---- */
/* A key, not a chip.
   This is the one element the whole site is named after, and it used to be
   a flat gradient rectangle used as mid-sentence punctuation. It now has
   the four things that make a key read as a physical object: a lit face, a
   side wall you can see beneath it, a defined edge, and its own contact
   shadow. `--kbd-depth` is how far it stands off the page, so a hero key
   and an inline one differ by one number rather than a second rule. */
.kbd {
  --kbd-depth: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.95em;
  height: 1.95em;
  padding: 0 0.52em;
  border-radius: 0.42em;
  background: var(--keycap-bg);
  border: 1px solid var(--keycap-border);
  box-shadow:
    inset 0 1px 0 var(--keycap-gloss),
    0 var(--kbd-depth) 0 -1px var(--keycap-wall),
    0 calc(var(--kbd-depth) + 1px) calc(var(--kbd-depth) + 2px) var(--keycap-ambient);
  color: var(--keycap-text);
  font-family: var(--font-mono);
  font-size: 0.85em;
  font-weight: 600;
  line-height: 1;
  vertical-align: middle;
  /* The legend sits a hair above optical centre, the way it does on a real
     keycap, where the face is dished. */
  padding-bottom: 0.06em;
}
.kbd + .kbd { margin-left: 0.24em; }
.kbd-row { display: inline-flex; align-items: center; }

/* Hero scale. Same object, more depth — a key you could press. */
.kbd-xl {
  --kbd-depth: 5px;
  font-size: 1em;
  min-width: 1.7em;
  height: 1.7em;
  border-radius: 0.3em;
  border-width: 1.5px;
}

/* Only keys that actually do something get the press. A keycap inside a
   sentence is a picture of a key and must not pretend to be interactive. */
a .kbd, button .kbd, .kbd-press {
  transition: transform 0.09s ease, box-shadow 0.09s ease;
}
a:active .kbd, button:active .kbd, .kbd-press:active {
  transform: translateY(calc(var(--kbd-depth) - 1px));
  box-shadow:
    inset 0 1px 0 var(--keycap-gloss),
    0 1px 0 -1px var(--keycap-wall),
    0 1px 2px var(--keycap-ambient);
}
@media (prefers-reduced-motion: reduce) {
  a .kbd, button .kbd, .kbd-press { transition: none; }
}

/* ---- Hero ---- */
/* Left-aligned and asymmetric, deliberately.
   This hero used to centre every element in it — eyebrow, headline, lede,
   button, footnote — which is the single most common tell of a page nobody
   made a layout decision about. Centred type also gives the reader a new
   left edge on every line, which is why it is reserved for short things.
   The copy now sits on one axis and the keys hang off the other. */
.hero {
  padding-block: 76px 56px;
}
.hero-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 44px;
  align-items: center;
}
@media (min-width: 900px) {
  .hero-grid { grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); gap: 56px; }
}
/* The hero's opening gesture. Four modifier keys at display scale with the
   default one held down — the product's whole premise, stated as an object
   rather than a sentence. It settles into the held state on load so the
   press is legible; both frames are readable, so a reader who arrives after
   it finishes has missed nothing. */
.hero-keys {
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  gap: 12px;
  font-size: clamp(1.5rem, 4.6vw, 3.1rem);
  gap: 0.2em;
  flex-wrap: wrap;
}
@media (min-width: 900px) {
  .hero-keys { justify-content: flex-end; align-items: flex-start; padding-top: 0.35em; }
}
.hero-keys .kbd {
  opacity: 0.42;
  transform: scale(0.88);
  transform-origin: top center;
}
.hero-keys .is-held {
  opacity: 1;
  transform: translateY(4px);
  box-shadow:
    inset 0 1px 0 var(--keycap-gloss),
    0 1px 0 -1px var(--keycap-wall),
    0 1px 3px var(--keycap-ambient);
  animation: key-press 620ms cubic-bezier(0.22, 1, 0.36, 1) 1;
}
@keyframes key-press {
  0%   { transform: translateY(-4px); }
  55%  { transform: translateY(6px); }
  100% { transform: translateY(4px); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-keys .is-held { animation: none; }
}
.hero h1 { max-width: 19ch; font-size: clamp(2.4rem, 5.6vw, 4.1rem); margin-bottom: 0.42em; }
.hero .lede { max-width: 46ch; font-size: 1.16rem; }
.hero .btn-row { margin-top: 30px; }
.hero-micro {
  margin-top: 20px;
  font-size: 0.85rem;
  color: var(--text-tertiary);
}
.hero-micro span:not(:last-child)::after { content: "·"; margin: 0 8px; color: var(--border-strong); }

/* The Homebrew one-liner under the download button, and the same command
   in the pricing card. A shell command is quoted, not prose, so it gets the
   mono face and a box.

   It wraps at its spaces on a narrow screen and nowhere else: `word-break`
   stays at its default so no token is ever split, which keeps
   `noloman/keysi/keysi` — the part a reader has to copy exactly — whole on
   one line. Wrapping at a space is safe to copy: the clipboard takes the
   text from the DOM, which has one space there, not the line break the
   browser drew. Refusing to wrap is what is not safe: at 375px it grew the
   pricing cards past the viewport and truncated the command mid-word. */
code.cmd {
  font-family: var(--font-mono);
  font-size: 0.86em;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
}
.hero-brew {
  margin-top: 14px;
  font-size: 0.9rem;
  color: var(--text-tertiary);
}

/* ---- Copy button on a command ----
   Injected by assets/js/copy.js, so none of this exists without it — the
   page with JavaScript off is the page as it was, not a dead button. */
.copy-wrap {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* The chip and its button stay on the same line as each other while the
     line as a whole still wraps at the command's own spaces. */
  max-width: 100%;
}
.copy-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 28px of ink, 44px of target: the padding is negative margin on the
     outside, so the button hits Apple's minimum tap size without pushing
     the line it sits on any taller. */
  width: 28px;
  height: 28px;
  padding: 8px;
  margin: -8px;
  box-sizing: content-box;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  color: var(--text-tertiary);
  cursor: pointer;
  transition: color 0.12s ease, background-color 0.12s ease;
}
.copy-btn:hover { color: var(--text); background: var(--bg-sunken); }
.copy-btn.is-copied { color: var(--good); }
.copy-btn svg { display: block; }

/* Announced, not drawn: the tick is invisible to a screen reader, and this
   is what it reads instead. Not `display: none`, which is not announced. */
.copy-status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* In a .verify-box the command is a block, so the button goes in the
   corner of the box rather than after the text. */
.copy-wrap-block {
  display: block;
  position: relative;
}
.copy-wrap-block .copy-btn {
  position: absolute;
  top: 0;
  right: 0;
  margin: 0;
  padding: 0;
  width: 30px;
  height: 30px;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
}
.copy-wrap-block .copy-btn:hover { background: var(--bg-elevated); }
.copy-wrap-block code { padding-right: 40px; }

@media (prefers-reduced-motion: reduce) {
  .copy-btn { transition: none; }
}

.hero-media {
  margin-top: 52px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  background: var(--bg-sunken);
}
.hero-media video, .hero-media img { width: 100%; height: auto; }

/* ---- Feature sections ---- */
.feature {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  align-items: center;
  padding-block: 56px;
  border-top: 1px solid var(--border);
}

@media (min-width: 860px) {
  .feature { grid-template-columns: 1fr 1fr; gap: 56px; }
  .feature.reverse .feature-copy { order: 2; }
  .feature.reverse .feature-media { order: 1; }
}
/* Feature eyebrows used to be accent-coloured; see the .tier note above for
   why the accent is now reserved for actions. */
.feature-copy .eyebrow { color: var(--text-tertiary); }
.feature-media {
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-md);
  background: var(--bg-sunken);
}
.feature-media img { width: 100%; height: auto; }

.callout {
  display: inline-block;
  margin-top: 14px;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  background: var(--warn-bg);
  border: 1px solid var(--warn-border);
  color: var(--warn-text);
  font-size: 0.88rem;
  font-family: var(--font-mono);
}
.verify-box {
  margin-top: 28px;
  padding: 16px 18px;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
/* On a doc page the boxes follow a sentence that introduces them, so they
   sit closer to it than the FAQ's stand-alone ones do. */
.doc .verify-box { margin-top: 12px; margin-bottom: 20px; }
.verify-box code {
  display: block;
  margin-top: 8px;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  overflow-x: auto;
  white-space: pre;
}

/* ---- Comparison table ---- */
.table-scroll { overflow-x: auto; margin-top: 28px; border: 1px solid var(--border); border-radius: var(--radius-md); }
table.compare { width: 100%; border-collapse: collapse; min-width: 620px; font-size: 0.92rem; }
table.compare th, table.compare td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); }
table.compare th { background: var(--bg-sunken); font-family: var(--font-display); font-weight: 700; }
/* The security page's inventory tables hold API names and paths, which have
   no spaces to wrap at. */
.doc table.compare td { vertical-align: top; }
.doc table.compare code { overflow-wrap: anywhere; }
table.compare td.yes { color: var(--good); font-weight: 600; }
table.compare td.no { color: var(--text-tertiary); }
table.compare tr:last-child td { border-bottom: none; }
.compare-date { font-size: 0.82rem; color: var(--text-tertiary); margin-top: 10px; }

/* ---- Pricing ---- */
/* Two cards since the move to freemium. Stacked, a desktop reader never
   sees the free tier and the paid one together — which is the entire
   comparison the section exists to make. */
.pricing-grid { display: grid; gap: 20px; margin-top: 40px; }
.pricing-card {
  max-width: 460px;
  width: 100%;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 32px 30px;
  box-shadow: var(--shadow-md);
}
.price-amount { font-family: var(--font-display); font-size: 3rem; font-weight: 700; margin: 8px 0 4px; }
.price-amount span { font-size: 1.1rem; color: var(--text-tertiary); font-weight: 500; }
.pricing-list { list-style: none; padding: 0; margin: 22px 0; display: grid; gap: 10px; }
.pricing-list li { display: flex; gap: 10px; font-size: 0.92rem; color: var(--text-secondary); }
.pricing-list .tick { color: var(--good); }
.pricing-card .btn-row { flex-direction: column; align-items: stretch; }
.pricing-card .btn { justify-content: center; }
.pricing-card .btn { justify-content: center; }
/* Grow the list, not the gaps, so both cards end at the same height and
   the two CTAs line up. */
.pricing-card .pricing-list { flex: 1; align-content: start; }
.free-summary-list { grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); column-gap: 24px; }
@media (min-width: 860px) { .pricing-grid { grid-template-columns: 1fr 1fr; gap: 24px; } }

/* ---- FAQ ---- */
.faq-item {
  border-bottom: 1px solid var(--border);
}
.faq-item summary {
  padding: 18px 0;
  cursor: pointer;
  font-weight: 600;
  font-family: var(--font-display);
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+";
  font-size: 1.3rem;
  color: var(--text-tertiary);
  flex-shrink: 0;
  transition: transform 0.15s ease;
}
.faq-item[open] summary::after { transform: rotate(45deg); }
.faq-item .faq-body { padding-bottom: 20px; color: var(--text-secondary); }
.faq-item .faq-body p:last-child { margin-bottom: 0; }

/* ---- Footer ---- */
footer.site-footer {
  border-top: 1px solid var(--border);
  padding-block: 40px;
  color: var(--text-tertiary);
  font-size: 0.88rem;
}
.footer-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px 24px;
  align-items: center;
  justify-content: space-between;
}
.footer-links { display: flex; flex-wrap: wrap; gap: 18px; list-style: none; margin: 0; padding: 0; }
.footer-links a { color: var(--text-secondary); text-decoration: none; }
.footer-links a:hover { color: var(--text); }
footer.site-footer .fine { margin-top: 18px; font-size: 0.82rem; }

/* ---- Long-form pages -----------------------------------------------
   Thirteen pages share this class and it used to be five declarations, so
   every page that was not the homepage read as a text file: no opener, no
   visible section starts, headings that floated in the middle of prose,
   and bulleted walls with nothing to hold the eye. The homepage got a
   design system and these never did.

   Rules and space do the work here rather than cards, matching the
   homepage. Nothing on this list is decoration: the rule above an h2 marks
   where a section begins, the lead paragraph marks the page's thesis, and
   the square bullet is the keycap's corner radius at 6px. */

.doc > .eyebrow { margin-bottom: 14px; }
.doc h1 {
  font-size: clamp(2.05rem, 4.6vw, 3.05rem);
  max-width: 22ch;
  margin-bottom: 0.34em;
}

/* The page's thesis, one paragraph, set larger than the body under it. */
.doc > p:first-of-type {
  font-size: 1.14rem;
  line-height: 1.5;
  color: var(--text-secondary);
  max-width: var(--measure);
}

/* An h2 starts a section, so it gets a rule and real space above it. */
.doc h2 {
  margin-top: 2.4em;
  padding-top: 1.05em;
  border-top: 1px solid var(--border);
  font-size: clamp(1.3rem, 2.7vw, 1.72rem);
}
.doc h3 { margin-top: 2em; margin-bottom: 0.4em; }
.doc p, .doc li { max-width: var(--measure); }

.doc ul, .doc ol { color: var(--text); padding-left: 0; }
.doc ol { padding-left: 1.3em; }
.doc li { margin-bottom: 0.55em; }
.doc ul { list-style: none; }
.doc ul > li { position: relative; padding-left: 1.5em; }
.doc ul > li::before {
  content: "";
  position: absolute;
  left: 0.15em;
  top: 0.6em;
  width: 6px;
  height: 6px;
  border-radius: 1.5px;
  background: var(--border-strong);
}

.doc .updated { color: var(--text-tertiary); font-size: 0.9rem; margin-top: -8px; }

/* Every one of these pages used to dead-end. */
.doc-foot {
  margin-top: 3em;
  padding-top: 26px;
  border-top: 1px solid var(--border-strong);
  display: flex;
  flex-wrap: wrap;
  gap: 14px 28px;
  align-items: center;
  justify-content: space-between;
}
.doc-foot p { margin: 0; max-width: 46ch; font-size: 0.95rem; color: var(--text-secondary); }
.doc-foot p strong { color: var(--text); }
/* A screenshot in a doc page's single-column flow, rather than beside copy
   in a `.feature` grid — it supplies the border and shadow, not the spacing. */
.doc .feature-media { margin-block: 28px; }

/* ---- Changelog ---- */
.release { padding-block: 28px; border-bottom: 1px solid var(--border); }
.release:last-child { border-bottom: none; }
.release h2 { display: flex; align-items: baseline; gap: 12px; font-size: 1.2rem; }
.release .date { font-size: 0.85rem; color: var(--text-tertiary); font-weight: 400; font-family: var(--font-mono); }
.release ul { color: var(--text-secondary); }

/* ---- 404 ---- */
.error-page { text-align: center; padding-block: 120px; }

/* ---- Utility ---- */
.center { text-align: center; }
.mt-0 { margin-top: 0; }
.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;
}

/* ================================================================
   Landing-page composition
   ================================================================
   The homepage used to be ten sections of equal weight, six of which
   argued the same free-vs-paid point, and six feature blocks rendered at
   identical size — so nothing on the page was ranked and scrolling told
   you nothing about what mattered. These components exist to rank it:
   one lead feature, a supporting pair, and a dense grid for everything
   that only needs a line. */

/* ---- Proof strip — replaces the 821px "why not the App Store" section.
   The same four facts, stated once, at the top, where they answer the
   question people actually arrive with. ---- */
.proof {
  background: var(--bg-elevated);
  border-block: 1px solid var(--border);
  padding-block: 18px;
}
.proof-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  list-style: none;
  margin: 0;
  padding: 0;
}
.proof-list li {
  font-size: 0.85rem;
  color: var(--text-secondary);
  padding: 6px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
}
.proof-list li strong { color: var(--text); font-weight: 600; }

/* ---- Origin note — two sentences where there used to be a five-paragraph
   section. The full account lives on cheatsheet-alternative.html. ---- */
.origin p { max-width: var(--measure); color: var(--text-secondary); }
.origin p strong { color: var(--text); }
.origin .fine { margin-top: 14px; }

/* ---- The one lead feature ----
   Full width, larger type, and two screenshots instead of one. The size
   difference is the ranking: this is what the $14.99 buys, and it used to
   occupy the same slot as every free feature. */
.feature-lead {
  border-top: 1px solid var(--border);
  padding-block: 48px 0;
}
.feature-lead .feature-copy { max-width: var(--measure); }
.feature-lead h2 {
  font-size: clamp(1.75rem, 4vw, 2.6rem);
  line-height: 1.12;
}
.feature-shots {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  margin-top: 34px;
}
@media (min-width: 760px) { .feature-shots { grid-template-columns: 1fr 1fr; gap: 28px; align-items: stretch; } }

.shot {
  margin: 0;
  /* The two lead shots have different aspect ratios, so in equal columns the
     images end at different heights. Stretching the figures and pushing the
     caption down with auto margin keeps the captions on one line with each
     other instead of stepping. */
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-sunken);
  box-shadow: var(--shadow-md);
}
.shot img { width: 100%; height: auto; }
.shot figcaption {
  margin-top: auto;
  padding: 10px 14px;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--text-tertiary);
  background: var(--bg-elevated);
  border-top: 1px solid var(--border);
}

/* ---- The supporting pair — half the width, half the type, one paragraph
   each. Two things no competitor's own site claims. ---- */
.feature-pair {
  display: grid;
  grid-template-columns: 1fr;
  gap: 44px;
  border-top: 1px solid var(--border);
  padding-block: 48px;
  margin-top: 48px;
}
@media (min-width: 760px) { .feature-pair { grid-template-columns: 1fr 1fr; gap: 48px; } }
.feature-pair h3 { font-size: 1.3rem; margin-bottom: 10px; }
.feature-pair p { color: var(--text-secondary); font-size: 0.95rem; }
.feature-pair .shot { margin-top: 18px; }

/* ---- Capability grid — one line per capability, grouped by what kind of
   thing it is. Absorbs the old "quiet details" strip and the three
   features that were being given a full screenshot each to say something
   a clause could say. ---- */
.capabilities {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  margin-top: 30px;
}
@media (min-width: 720px) { .capabilities { grid-template-columns: repeat(3, 1fr); gap: 40px; } }
.cap-group h3 {
  font-family: var(--font-label);
  font-size: 0.62rem;
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: 0 0 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.cap-group ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 13px;
}
.cap-group li { font-size: 0.92rem; line-height: 1.45; color: var(--text-secondary); }
.cap-group li strong { display: block; color: var(--text); font-weight: 600; }

/* ---- Handoff to a page that covers a topic properly ---- */
.handoff {
  display: flex;
  flex-wrap: wrap;
  gap: 14px 32px;
  align-items: baseline;
  justify-content: space-between;
  padding-block: 22px;
  border-top: 1px solid var(--border-strong);
  border-bottom: 1px solid var(--border);
}
.handoff p { margin: 0; max-width: 54ch; font-size: 0.95rem; color: var(--text-secondary); }
.handoff p strong { color: var(--text); }

/* ---- A working day, before and after — what changes for the person,
   ahead of the features that do it. One row per outcome: the outcome, the
   day without Keysi, the day with it. The labels are words, not colour, so
   the contrast between the columns survives a screen reader and greyscale.
   The "after" side is the one in full-strength text. ---- */
.day-list { list-style: none; margin: 30px 0 0; padding: 0; }
.day-item {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px 32px;
  padding-block: 22px;
  border-top: 1px solid var(--border);
}
@media (min-width: 760px) { .day-item { grid-template-columns: 15rem 1fr 1fr; } }
.day-item h3 { font-size: 1.05rem; margin: 0; }
.day-item p { margin: 0; font-size: 0.93rem; line-height: 1.5; color: var(--text-secondary); }
.day-item .day-after { color: var(--text); }
.day-label {
  display: block;
  font-family: var(--font-label);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: 6px;
}
.day-compare {
  max-width: var(--measure);
  padding-top: 22px;
  border-top: 1px solid var(--border);
  font-size: 0.95rem;
  color: var(--text-secondary);
}

/* ---- Footer link map. The homepage now hands four topics off to other
   pages, so the footer has to actually route people to them. ---- */
.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  gap: 28px 24px;
}
.footer-col h3 {
  font-family: var(--font-label);
  font-size: 0.6rem;
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: 0 0 12px;
}
.footer-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.footer-col a { color: var(--text-secondary); text-decoration: none; font-size: 0.9rem; }
.footer-col a:hover { color: var(--text); }
.footer-base {
  margin-top: 32px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 10px 20px;
  justify-content: space-between;
  font-size: 0.82rem;
}

/* The FAQ absorbed the App Store and hold-vs-panel sections, so its answers
   are now the longest prose on the page and need the same cap as the rest. */
.faq-item .faq-body { max-width: var(--measure); }

/* ================================================================
   The page's own hold-⌘ overlay (assets/js/hold.js)
   ================================================================
   Styled to look like the panel Keysi actually draws: a translucent
   floating sheet, rows grouped the way a menu bar is grouped. It is the
   one element on this site that could not have come from a template,
   because it only makes sense for this product.

   It sits above everything and takes no space until JS opens it, so the
   page is unchanged for anyone without JavaScript. */
.hold-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
  background: color-mix(in srgb, var(--bg-sunken) 55%, transparent);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease, visibility 0s linear 0.12s;
}
.hold-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.12s ease, visibility 0s;
}

.hold-panel {
  width: min(640px, 100%);
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-strong);
  background: color-mix(in srgb, var(--bg-elevated) 92%, transparent);
  backdrop-filter: saturate(160%) blur(22px);
  -webkit-backdrop-filter: saturate(160%) blur(22px);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  transform: translateY(6px) scale(0.985);
  transition: transform 0.14s cubic-bezier(0.22, 1, 0.36, 1);
}
.hold-overlay.is-open .hold-panel { transform: none; }

.hold-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
}
.hold-app {
  font-family: var(--font-label);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.hold-held {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 0.8rem;
  color: var(--text-tertiary);
}

.hold-body {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px 24px;
  padding: 14px 10px;
}
@media (min-width: 560px) { .hold-body { grid-template-columns: repeat(3, 1fr); } }

.hold-group-title {
  font-family: var(--font-label);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  padding: 8px 10px 6px;
}
.hold-row {
  display: block;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
  color: var(--text);
  text-decoration: none;
}
.hold-row:hover {
  background: var(--accent);
  color: var(--accent-text);
}

.hold-foot {
  padding: 12px 18px 14px;
  border-top: 1px solid var(--border);
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-tertiary);
  background: var(--bg-sunken);
}

@media (prefers-reduced-motion: reduce) {
  .hold-overlay, .hold-panel { transition: none; }
}

/* The invitation. Small, once, under the hero's buttons — a page that does
   something on a keystroke has to say so, or nobody will ever find it. */
.hold-hint {
  display: none;
  margin-top: 16px;
  font-size: 0.82rem;
  color: var(--text-tertiary);
}
@media (hover: hover) and (pointer: fine) { .hold-hint { display: block; } }
.hold-hint .kbd { font-size: 0.78em; }

/* A screenshot in a long-form page's single column. */
.doc-shot { margin-block: 30px; }
.doc-shot figcaption { font-size: 0.82rem; }

/* 404's three keys, at the same scale as the hero's. */
.error-keys { justify-content: center; font-size: clamp(1.6rem, 6vw, 2.6rem); margin-bottom: 26px; gap: 0.2em; }

/* Pricing-card text roles, and the FAQ column. These were seven inline
   `style` attributes, which is where a design system goes to die: the values
   could not be reused, could not be themed, and could not be found. */
.price-sub { color: var(--text-secondary); margin-bottom: 0; }
.price-note { color: var(--text-secondary); font-size: 0.85rem; margin: 0 0 0.5rem; }
.price-after { color: var(--text-secondary); font-size: 0.9rem; margin: 0.75rem 0 0; }
.compare-links { margin-top: 14px; }
.faq-list { max-width: 66ch; margin-top: 28px; }
.error-lede { margin: 0 auto 28px; }
.error-actions { justify-content: center; }

/* ---- Language switcher and suggestion banner ------------------------
   Nine languages in the app, one on the site until now. Two surfaces:

   The switcher is server-rendered by scripts/site-i18n.py into the footer
   of every page that has a translation, as real <a> links. It has to work
   with JavaScript off, and it has to be crawlable — those links are the
   internal linking that tells a crawler the locale pages exist. It sits in
   the footer rather than the nav on purpose: a reader who wants another
   language will look for it, and a reader who doesn't shouldn't have the
   nav's one clear call to action competing with a list of languages.

   The banner is built by assets/js/i18n.js and only ever *offers*. It is
   styled like the consent banner above deliberately — same corner, same
   weight, same dismissibility — because it is the same kind of thing: a
   one-time question the page asks and then stops asking. ---- */
.lang-switcher {
  margin-top: 20px;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 14px;
  font-size: 0.82rem;
}
.lang-switcher-label {
  font-family: var(--font-label);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.lang-switcher ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
}
.lang-switcher a { color: var(--text-secondary); text-decoration: none; }
.lang-switcher a:hover { color: var(--text); text-decoration: underline; }
/* The current language is stated, not offered. Still a link, so it is still
   focusable and still tells a crawler this URL is the one it is on. */
.lang-switcher a[aria-current] { color: var(--text); cursor: default; }
.lang-switcher a[aria-current]:hover { text-decoration: none; }

.lang-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 99; /* Under .consent-banner: consent is the question that has
                  to be answered first, and both are bottom-anchored. */
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  font-size: 0.88rem;
}
.lang-banner-link { color: var(--accent); font-weight: 500; }
@media (min-width: 640px) {
  /* Opposite corner to the consent banner, so the two can be on screen at
     once without one hiding the other. */
  .lang-banner { right: auto; max-width: 420px; }
}
