/* ---------------------------------------------------------------------------
   Alpha-OSK marketing site
   ---------------------------------------------------------------------------
   Dark-first, because the app is. The palette below is lifted from the app's
   own Dark theme (qml/Main.qml::themeData) so the site and the product read as
   one thing: background #1a1a1a, key #3a3a3a, accent #4a9eff.

   This site advertises an accessibility tool, so the site itself has to clear
   the bar it is selling:
     - every interactive target is at least 44x44 CSS px, most are larger
     - focus is always visible, and never removed without a replacement
     - nothing is conveyed by colour alone
     - every animation is off under prefers-reduced-motion
     - text contrast clears WCAG AA against its own surface, in both themes
   Changing a colour here means re-checking the pair it sits on.
--------------------------------------------------------------------------- */

/* ---------- Tokens ---------- */

:root {
  color-scheme: dark;

  /* Surfaces, darkest to lightest. */
  --bg: #0e0e10;
  --bg-alt: #141417;
  --surface: #1a1a1e;
  --surface-2: #232329;
  --key: #3a3a3a;

  /* Ink. --muted is the lightest grey that still clears 4.5:1 on --bg. */
  --ink: #ededf0;
  --ink-muted: #a8b0bd;
  --ink-faint: #7d8593;

  --accent: #4a9eff;
  --accent-strong: #6fb2ff;
  --accent-ink: #05213f;
  --accent-wash: rgba(74, 158, 255, 0.12);

  --good: #4ade80;
  --warn: #fbbf24;

  --border: #2c2c33;
  --border-strong: #3d3d46;

  --radius: 14px;
  --radius-sm: 9px;
  --radius-lg: 22px;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.35);
  --shadow-lg: 0 2px 6px rgba(0, 0, 0, 0.45), 0 28px 70px rgba(0, 0, 0, 0.5);

  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;

  --measure: 68ch;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --page: 1180px;
  --section-y: clamp(4.5rem, 9vw, 8rem);

  /* The gutter a phone actually has. In landscape on a notched device the
     display cutout and the home indicator sit inside the viewport, and content
     laid out to the raw edge lands underneath them. Every horizontal edge on
     this page is derived from these rather than from --gutter directly, so the
     insets survive the negative margin .kbdemo uses to escape the wrap. */
  --gutter-left: calc(var(--gutter) + env(safe-area-inset-left, 0px));
  --gutter-right: calc(var(--gutter) + env(safe-area-inset-right, 0px));

  /* The flash a tap leaves behind on a touchscreen. */
  --tap: rgba(74, 158, 255, 0.22);

  /* The two colour fields drifting behind the hero. The blue is the accent;
     the violet is a nod to the app's Amethyst theme and is there so the two
     fields separate as they cross rather than reading as one brighter blue.
     Both alphas are set by measurement, not by eye: see the note on .hero. */
  --aura-1: rgba(74, 158, 255, 0.20);
  --aura-2: rgba(124, 92, 255, 0.16);
}

[data-theme="light"] {
  color-scheme: light;

  --bg: #f7f7f9;
  --bg-alt: #ffffff;
  --surface: #ffffff;
  --surface-2: #f0f1f4;
  --key: #d8dade;

  --ink: #14161a;
  --ink-muted: #4d5563;
  --ink-faint: #6b7280;

  --accent: #0b62d0;
  --accent-strong: #084da6;
  --accent-ink: #ffffff;
  --accent-wash: rgba(11, 98, 208, 0.09);

  --good: #15803d;
  --warn: #a16207;

  --border: #e0e2e7;
  --border-strong: #c9ccd3;

  --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 10px 28px rgba(16, 24, 40, 0.08);
  --shadow-lg: 0 2px 6px rgba(16, 24, 40, 0.08), 0 26px 64px rgba(16, 24, 40, 0.12);

  --tap: rgba(11, 98, 208, 0.18);

  /* Lower alphas than the dark theme for the same apparent strength. A wash
     over white lightens toward the page background and loses contrast against
     dark text; the same wash over near-black is mostly additive and barely
     moves the effective background under light text. */
  --aura-1: rgba(11, 98, 208, 0.10);
  --aura-2: rgba(91, 63, 209, 0.07);
}

/* ---------- Reset ---------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* Anchored headings must not land under the sticky header. */
:target {
  scroll-margin-top: 6rem;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.075rem);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* Only bites when a word would otherwise run past its own box, which on a
     320px viewport at a doubled font size is the h1 ("smartest" alone is
     wider than the screen at 4.1rem of a 32px root) and the config paths in
     the FAQ. Without it those spill out of the page rather than wrapping, and
     the whole document picks up a horizontal scrollbar to reach text that is
     already unreadable at the edge: 1.4.4 again, from the other direction.
     break-word rather than anywhere, so ordinary prose is never broken mid
     word while a break opportunity still exists. */
  overflow-wrap: break-word;
}

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

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

h1 {
  font-size: clamp(2.35rem, 1.5rem + 3.6vw, 4.1rem);
  font-weight: 800;
  letter-spacing: -0.035em;
}

h2 {
  font-size: clamp(1.8rem, 1.3rem + 2vw, 2.7rem);
  font-weight: 750;
}

h3 {
  font-size: clamp(1.15rem, 1.05rem + 0.4vw, 1.35rem);
  font-weight: 650;
}

p {
  margin: 0 0 1.1em;
  max-width: var(--measure);
  text-wrap: pretty;
}

a {
  color: var(--accent);
  text-underline-offset: 0.22em;
  text-decoration-thickness: 0.08em;
}

@media (hover: hover) {
  a:hover {
    color: var(--accent-strong);
  }
}

code {
  font-family: var(--mono);
  font-size: 0.88em;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.12em 0.42em;
}

/* One focus style everywhere, never removed. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* The one exception, and only because #main is not a control. The skip link
   moves focus here so the next Tab continues into the content; the element is
   the full height of the page, so the shared ring would draw a rectangle down
   both viewport edges and read as a rendering fault. The scroll and the
   changed tab position are the affordance. Do not extend this to anything the
   user can actually operate. */
#main:focus,
#main:focus-visible {
  outline: none;
}

::selection {
  background: var(--accent);
  color: var(--accent-ink);
}

/* ---------- Touch ---------- */

/* touch-action: manipulation drops the ~300ms a browser spends deciding
   whether a tap was the first half of a double-tap-to-zoom. It is named on the
   controls one at a time rather than on the page, because the page as a whole
   has to keep every zoom gesture it has: pinch zoom is the most used
   accessibility feature on a phone, and nothing on this site is allowed to
   take it away. */
.btn,
.nav a,
.nav-toggle,
.theme-toggle,
.skip-link,
.faq summary,
.footer-col a,
.footer-bottom a,
.kbdemo__key,
.kbdemo__pill,
.kbdemo__clear {
  touch-action: manipulation;
}

/* The tap flash. Chrome's default is a grey rectangle that squares off every
   rounded corner on the page; --tap is the accent the rest of the site already
   uses to say "you hit this". Not transparent: taking the flash away without
   putting something back is how a tap comes to feel like it did not register,
   and this is a site about knowing your click landed. */
a,
button,
summary,
[role="button"] {
  -webkit-tap-highlight-color: var(--tap);
}

/* ---------- Layout helpers ---------- */

.wrap {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-left: var(--gutter-left);
  padding-right: var(--gutter-right);
}

.section {
  padding-block: var(--section-y);
}

.section--alt {
  background: var(--bg-alt);
  border-block: 1px solid var(--border);
}

/* A phone shows roughly a third of what a desktop does per screen, so the
   desktop's breathing room between sections turns into several swipes through
   nothing at all. The floor of the clamp is the only part that a phone ever
   reaches, so this is the only number worth changing. */
@media (max-width: 560px) {
  :root {
    --section-y: 3.5rem;
  }
}

.section__head {
  max-width: 46rem;
  margin-bottom: clamp(2.25rem, 4vw, 3.5rem);
}

.section__head p {
  color: var(--ink-muted);
  font-size: 1.1em;
  margin-bottom: 0;
}

/* The rule above drops the trailing margin so the head sits tight against
   whatever follows it, which is right for the six section heads that are one
   paragraph long and wrong for the one that is not: Privacy runs "where the
   data is" straight into "what is refused" with no gap, so two arguments read
   as one wall. Restore the space between them without giving the last one a
   trailing margin back. */
.section__head p + p {
  margin-top: 1.1em;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.85rem;
}

.eyebrow::before {
  content: "";
  width: 1.6rem;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
}

.skip-link {
  position: absolute;
  left: 0.75rem;
  top: -100%;
  z-index: 200;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  padding: 0.85rem 1.4rem;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  text-decoration: none;
}

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

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Buttons ---------- */

.btn {
  --btn-bg: var(--surface-2);
  --btn-ink: var(--ink);
  --btn-border: var(--border-strong);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 3.4rem;
  padding: 0.85rem 1.55rem;
  background: var(--btn-bg);
  color: var(--btn-ink);
  border: 1.5px solid var(--btn-border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-weight: 650;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.14s ease, background-color 0.14s ease, border-color 0.14s ease;
}

@media (hover: hover) {
  .btn:hover {
    transform: translateY(-2px);
    color: var(--btn-ink);
  }
}

.btn:active {
  transform: translateY(0);
}

.btn--primary {
  --btn-bg: var(--accent);
  --btn-ink: var(--accent-ink);
  --btn-border: var(--accent);
  font-weight: 700;
  box-shadow: var(--shadow);
}

@media (hover: hover) {
  .btn--primary:hover {
    --btn-bg: var(--accent-strong);
    --btn-border: var(--accent-strong);
  }
}

.btn--lg {
  min-height: 4rem;
  padding: 1rem 2rem;
  font-size: 1.075rem;
}

.btn__sub {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  opacity: 0.85;
  letter-spacing: 0.01em;
}

.btn__stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  text-align: left;
}

.btn__icon {
  flex: none;
  width: 1.35em;
  height: 1.35em;
  fill: currentColor;
}

/* ---------- Header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 4.5rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-weight: 800;
  font-size: 1.12rem;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-decoration: none;
  margin-right: auto;
  padding: 0.35rem;
  border-radius: var(--radius-sm);

  /* The header's own link home, and the one control up here that was under
     the 44px this file claims for every target: 39px on a phone, 43px on a
     tablet, both from the icon rather than from any deliberate size. It sits
     inside a 72px row, so the height was free. */
  min-height: 2.75rem;

  /* Without this the name breaks after the hyphen at any phone width, and a
     sticky header that is 72px tall on a desktop becomes 145px: a sixth of an
     iPhone viewport, held there for the length of the page. */
  white-space: nowrap;
}

.brand img {
  width: 2rem;
  height: 2rem;
  border-radius: 7px;
}

.nav {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.nav a:not(.btn) {
  display: inline-flex;
  align-items: center;
  min-height: 2.85rem;
  padding: 0.4rem 0.85rem;
  border-radius: var(--radius-sm);
  color: var(--ink-muted);
  font-weight: 600;
  font-size: 0.95rem;
  text-decoration: none;
}

@media (hover: hover) {
  .nav a:not(.btn):hover {
    color: var(--ink);
    background: var(--surface-2);
  }
}

/* The panel's own Download call to action. It exists only on the narrow
   screens where the header itself has no room to carry one; see the 600px
   query at the end of this section. */
.nav__cta {
  display: none;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.nav-toggle,
.theme-toggle {
  align-items: center;
  justify-content: center;
  width: 2.85rem;
  height: 2.85rem;
  padding: 0;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border-strong);
  background: var(--surface);
  color: var(--ink-muted);
  cursor: pointer;
}

.theme-toggle {
  display: inline-flex;
}

/* The menu button is the only affordance for a panel that nothing but script
   can open, so it stays out of the document until both of its conditions hold:
   the script is running, and the viewport is too narrow for the inline nav.
   Both of those live in the 860px query below. */
.nav-toggle {
  display: none;
}

@media (hover: hover) {
  .nav-toggle:hover,
  .theme-toggle:hover {
    color: var(--ink);
    border-color: var(--accent);
  }
}

.nav-toggle svg,
.theme-toggle svg {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.nav-toggle svg {
  width: 1.3rem;
  height: 1.3rem;
}

/* Show exactly one of the two icons, whichever names the theme you would
   switch TO, so the control reads as an action rather than a status. */
[data-theme="light"] .theme-toggle .icon-sun,
.theme-toggle .icon-moon {
  display: none;
}

[data-theme="light"] .theme-toggle .icon-moon {
  display: block;
}

/* The menu button says the same thing a different way: bars mean open, the
   cross means close, and aria-expanded on the button itself carries it to
   anyone who cannot see either glyph. */
.nav-toggle .icon-close {
  display: none;
}

.nav-toggle[aria-expanded="true"] .icon-bars {
  display: none;
}

.nav-toggle[aria-expanded="true"] .icon-close {
  display: block;
}

/* ---------- Header, narrower than the inline nav needs ---------- */

/* Five links plus a brand and two controls stop fitting on one line at about
   this width. They used to be set to display: none here with nothing put in
   their place, so every phone and most tablets got a header containing no
   navigation at all: How it works, Features, Privacy, FAQ and Source were
   reachable only by scrolling the whole page or by finding them again in the
   footer. On the site whose argument is that pointing should be cheap, that
   was the most expensive thing on it.

   They now fold into a disclosure panel. Opening it needs script, so the fold
   itself is conditional on .js, which the inline head script sets before first
   paint. With scripts off the links stay in the header and wrap onto a second
   row: taller, but every link is present and still 45px high.

   In em, for the reason the footer's 23.5em query gives at length: this asks
   whether six links and three controls still fit on a line, which is a
   question about the text, not about the device. In px it was answered once,
   at a 16px root, and then held whatever the reader's own font size was. A
   visitor browsing at a 32px default got the desktop header on a 1280px
   screen, and the row ran 358px past the right edge of the viewport with no
   way to reach the end of it: a 1.4.4 failure, on the setting a good part of
   this site's audience is most likely to have changed. 53.75em is 860px at a
   default root, so nothing moves for a reader who has not changed one, and
   the fold arrives on its own once the text is large enough to need it. */
@media (max-width: 53.75em) {
  .site-header__inner {
    flex-wrap: wrap;
    row-gap: 0.35rem;
    padding-block: 0.5rem;
  }

  /* The scripts-off case. The links are still in the flow here, and a row of
     five of them is wider than a phone, so give them a second row of their own
     and let them wrap inside it. Without both of these the header runs past
     the right edge of the screen and takes the whole page with it. The .js
     rules below take the panel back out of the flow, where none of this
     applies. */
  .nav {
    flex-basis: 100%;
    flex-wrap: wrap;
    gap: 0.15rem;
  }

  .js .nav-toggle {
    display: inline-flex;
  }

  .js .nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0.15rem;
    padding-top: 0.5rem;
    padding-left: var(--gutter-left);
    padding-right: var(--gutter-right);
    padding-bottom: calc(0.9rem + env(safe-area-inset-bottom, 0px));
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow);

    /* A phone in landscape has less room under the header than this panel
       wants. Scroll the panel rather than the page behind it. The percentage
       in the calc is the header's own height: the panel's containing block is
       .site-header, which is sticky and therefore positioned. */
    max-height: calc(100vh - 100%);
    max-height: calc(100dvh - 100%);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .js .nav[data-open] {
    display: flex;
  }

  /* Full width and taller than the header row's version. This is a list meant
     to be hit with a thumb, and nothing is gained by keeping the targets small
     when the whole width is there to spend. */
  .js .nav a:not(.btn) {
    min-height: 3.2rem;
    padding-inline: 0.9rem;
    font-size: 1.05rem;
    color: var(--ink);
  }
}

/* Below this a brand, a menu button, a theme button and a Download button stop
   fitting on one line at 320px, the narrowest viewport this site claims to
   support. Download is the one that moves, to the top of the menu panel, since
   it is the only one of the four already repeated three more times down the
   page. In em for the same reason as the query above: what stops fitting is
   the word "Download" beside the word "Alpha-OSK", and both of those grow with
   the reader's font size. 37.5em is 600px at a default root. */
@media (max-width: 37.5em) {
  .js .header-cta {
    display: none;
  }

  .js .nav__cta {
    display: inline-flex;
    width: 100%;
    margin-bottom: 0.35rem;
  }
}

/* The last of the three, and em for the same reason: this shrinks the brand
   because the brand has stopped fitting beside the two controls, which is a
   fact about the word rather than about the screen. 25em is 400px. */
@media (max-width: 25em) {
  .site-header__inner {
    gap: 0.5rem;
    min-height: 4rem;
  }

  .brand {
    font-size: 1.02rem;
    gap: 0.55rem;
  }

  .brand img {
    width: 1.75rem;
    height: 1.75rem;
  }
}

/* ---------- Hero ---------- */

.hero {
  position: relative;
  padding-top: clamp(3.5rem, 7vw, 6rem);
  padding-bottom: clamp(2rem, 4vw, 3rem);
  overflow: hidden;

  /* Gives the two aura layers below a stacking context of their own, so that
     z-index: -1 puts them behind the hero's content without putting them
     behind the page. */
  isolation: isolate;
}

/* Two accent blooms drifting behind the hero, replacing the single static one
   that used to live on ::before.

   Decorative, but NOT contrast-neutral: they lift the effective background
   enough to drag --ink-faint under AA, which is why .hero__note below picks a
   stronger ink by hand. Any muted text placed inside the hero has to be
   measured against the blooms, not against --bg, and measured at more than one
   moment now that they move. The alphas in --aura-1 and --aura-2 are set so
   that the worst frame, where both fields overlap directly under the lede,
   still clears AA in both themes. Raising either one means re-measuring; the
   check is in the commit that added this.

   Only transform is animated. A drifting background-position or an animated
   filter would repaint a viewport-sized area every frame, which on the kind of
   hardware a lot of this site's audience is running is a real cost for a
   decoration. A transform on a layer the compositor already holds is close to
   free, and it is the reason these are two oversized boxes carrying a gradient
   rather than two blurred shapes. */
.hero::before,
.hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  pointer-events: none;

  /* Oversized on every side so that no edge of either box can drift into
     view, whatever point of its travel it is at. */
  inset: -30% -20% -10%;
}

.hero::before {
  background: radial-gradient(42% 46% at 34% 34%, var(--aura-1), transparent 68%);
  animation: aura-a 34s ease-in-out infinite alternate;
}

.hero::after {
  background: radial-gradient(38% 42% at 68% 46%, var(--aura-2), transparent 66%);
  animation: aura-b 47s ease-in-out infinite alternate;
}

/* Two durations that do not divide into each other, so the pair does not
   return to the same arrangement for about twenty minutes. Nobody will watch
   it for twenty minutes; the point is that a visitor who glances back at the
   top of the page does not find the exact picture they left. */
@keyframes aura-a {
  from {
    transform: translate3d(-4%, -2%, 0) scale(1);
  }

  to {
    transform: translate3d(5%, 3%, 0) scale(1.14);
  }
}

@keyframes aura-b {
  from {
    transform: translate3d(4%, 2%, 0) scale(1.1);
  }

  to {
    transform: translate3d(-5%, -3%, 0) scale(0.94);
  }
}

/* Scrolled past, the hero is not on screen and the compositor has no reason to
   keep waking up for it. initAura in scripts/alphaosk.js sets the attribute.
   Without script the animation simply runs, which is the old behaviour of any
   CSS animation and costs a phone battery very little at this size. */
.hero[data-aura-paused]::before,
.hero[data-aura-paused]::after {
  animation-play-state: paused;
}

.hero__inner {
  position: relative;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* align-items: center leaves these sized to their own content rather than to
   the column, and a block whose content cannot be broken is as wide as the
   longest thing in it. At a doubled default font size on a 320px screen the h1
   is one 75px word, "smartest", which is wider than the whole viewport: the
   heading sized itself to 329px, sat centred in a 320px column and hung five
   pixels off both edges, and the page grew a horizontal scrollbar to reach a
   headline that could not be read at either end.

   Capping the item is what lets overflow-wrap on body do its job, since a word
   only breaks once the box it is in refuses to grow for it. */
.hero__inner > * {
  max-width: 100%;
}

.hero h1 {
  margin-bottom: 0.35em;
}

.hero__lede {
  font-size: clamp(1.1rem, 1rem + 0.55vw, 1.35rem);
  color: var(--ink-muted);
  max-width: 46rem;
  margin-inline: auto;
}

.badge-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.85rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: 0.83rem;
  font-weight: 600;
}

.badge__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--good);
  flex: none;
}

.hero__cta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.85rem;
  margin-top: 1.9rem;
}

/* Wrapped, the two buttons stack at their own natural widths, which leaves the
   second one narrower and centred under the first. That reads as the lesser of
   two options when it is only the second of them. One width for both. The
   CTA band and the 404 page reuse this class and want the same thing. */
@media (max-width: 560px) {
  .hero__cta {
    flex-direction: column;
    align-items: stretch;
    align-self: stretch;
    width: 100%;
  }

  .hero__cta .btn {
    width: 100%;
  }
}

.hero__note {
  margin-top: 1.15rem;
  font-size: 0.9rem;
  /* NOT --ink-faint. This paragraph sits on the hero's accent bloom, and
     compositing that wash over the page background lifts the effective
     background enough that --ink-faint measures 4.45:1 in dark and 3.98:1 in
     light, both under the 4.5:1 AA needs at this size. --ink-muted is 7.6:1 and
     6.2:1 on the same background. axe cannot catch this: it returns
     "incomplete" rather than a violation whenever the background it would have
     to sample is a gradient. */
  color: var(--ink-muted);
  max-width: 40rem;
}

/* ---------- Hero usage figures ---------- */

.hero__usage {
  display: flex;
  flex-wrap: wrap;
  /* The hero column centres this block, but that only centres the box. Once
     the two figures wrap onto separate lines the narrower one would sit at the
     left edge of the wider one, which reads as a misalignment against a hero
     that is centred everywhere else. */
  justify-content: center;
  gap: 1.25rem 2.75rem;
  margin: 1.85rem 0 0;
}

/* The value reads above its label, but a <dl> requires the term first and the
   term IS the label, so the visual order is reversed here rather than the
   markup being bent to suit it. */
.usage {
  display: flex;
  flex-direction: column-reverse;
}

.usage__value {
  margin: 0;
  font-size: clamp(2.3rem, 1.5rem + 3.2vw, 3.4rem);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--ink);
  /* Tabular figures because these numbers are replaced after a fetch. Without
     them a correction from 101,533 to 98,204 shifts the label under it. */
  font-variant-numeric: tabular-nums;
}

.usage__label {
  margin-top: 0.4rem;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* --ink-muted rather than --ink-faint for the reason given on .hero__note:
     this sits on the same accent bloom, where --ink-faint falls under AA. */
  color: var(--ink-muted);
}

.hero__source {
  margin-top: 1.3rem;
  font-size: 0.85rem;
  color: var(--ink-muted);
  max-width: 44rem;
}

.hero__shot {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  padding: 0.6rem;
  box-shadow: var(--shadow-lg);
}

.hero__shot img {
  border-radius: calc(var(--radius-lg) - 8px);
  width: 100%;
}

/* ---------- Stat strip ---------- */

/* Every auto-fit grid below wraps its minimum in min(..., 100%). Without it a
   track whose minimum exceeds the viewport overflows the page rather than
   collapsing, which is a horizontal scrollbar on a phone and nothing at all on
   a desktop, so it is easy to ship. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.stat {
  background: var(--bg-alt);
  padding: 1.5rem 1.35rem;
}

.stat__value {
  display: block;
  font-size: clamp(1.6rem, 1.3rem + 1vw, 2.1rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--ink);
  line-height: 1.1;
}

.stat__label {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

/* ---------- Live prediction demo ---------- */

/* The block stacks: the explanation, then the thing you can actually touch.
   The previous scripted panel sat in a second column beside the prose, which
   left it half width. That is the wrong trade for a panel whose whole argument
   is that its targets are big enough to hit, so the keyboard gets the full
   measure and the copy runs above it. */
.demo__copy {
  /* Same 46rem as the panel below it, so the two share a left edge. The
     paragraphs were already capping at 68ch, which lands within a few pixels of
     this, so nothing about the line length changes. */
  max-width: 46rem;
  margin-inline: auto;
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

.demo__copy p:last-child {
  margin-bottom: 0;
}


/* The panel is a mock of the application's own window, so it keeps the app's
   Dark theme colours in both site themes rather than following the page. It is
   a picture of a device, not a piece of this page's chrome. */
.kbdemo {
  max-width: 46rem;
  margin-inline: auto;
  background: #1a1a1a;
  border: 1px solid #505050;
  border-radius: var(--radius);
  padding: 1.1rem;
  box-shadow: var(--shadow-lg);
}

/* Set by alphaosk.js the moment it runs, and never removed. 26rem is the built
   panel rounded up: it measures 415px wide and 397px at 360px, so this holds
   every width at one height and the keyboard appearing inside it changes no
   layout at all. Reserved rather than animated away because the alternative is
   every anchor below this panel landing a third of a screen off. */
.kbdemo[data-demo-reserved] {
  min-height: 26rem;
}

/* The panel holds its dark surfaces in both site themes, so the page's accent
   is the wrong focus colour inside it. In light theme --accent is #0b62d0,
   which is about 2:1 against the #3a3a3a keys and pills and fails 1.4.11 on
   the 34 buttons here. Use the panel's own accent, the blue the pills already
   take on hover: 4.05:1 on a key, 6.3:1 on the panel ground. */
.kbdemo :focus-visible {
  outline-color: #4a9eff;
}

/* The markup keeps this in the document from first paint so that role="status"
   is a live region by the time demo.js has anything to put in it. Empty, it is
   still a bordered amber box, so hide it until it says something. */
.kbdemo__status:empty {
  display: none;
}

.kbdemo__status {
  margin: 0 0 0.9rem;
  padding: 0.75rem 0.9rem;
  border-radius: var(--radius-sm);
  background: #3a2a12;
  border: 1px solid #7a5a1a;
  color: #f3d9a4;
  font-size: 0.88rem;
  max-width: none;
}

/* Stands in for the application you are typing into. Focusable, so somebody on
   a physical keyboard can drive the demo with it. */
.kbdemo__screen {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 4rem;
  padding: 0.9rem 1.05rem;
  background: #0d0d0d;
  border: 1px solid #333;
  border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: 1.1rem;
  color: #e0e0e0;
  overflow: hidden;
  cursor: text;
}

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

/* No text-overflow here on purpose. This is a text field, and a field keeps
   the insertion point in view: renderText scrolls it to the end instead. An
   ellipsis truncated the newest characters, so once the sentence outgrew the
   panel the display froze on the oldest words and typing changed nothing. */
.kbdemo__text {
  white-space: pre;
  overflow: hidden;
  direction: ltr;
}

/* Shown until demo.js reports ready, hidden once it does or once it has said
   what went wrong. 11.8:1 on the panel ground. */
.kbdemo__fallback {
  margin: 0;
  max-width: none;
  color: #cfd6e0;
  font-size: 0.95rem;
  line-height: 1.55;
}

.kbdemo[data-demo-ready] .kbdemo__fallback,
.kbdemo[data-demo-broken] .kbdemo__fallback {
  display: none;
}

/* Keys, pills and the screen are all built in script. Until that has happened
   they are empty boxes and a Clear button that does nothing, so they stay out
   of the document rather than presenting a dead control surface. */
.kbdemo:not([data-demo-ready]) .kbdemo__screen,
.kbdemo:not([data-demo-ready]) .kbdemo__pills,
.kbdemo:not([data-demo-ready]) .kbdemo__keys,
.kbdemo:not([data-demo-ready]) .kbdemo__foot {
  display: none;
}

.kbdemo__caret {
  flex: none;
  width: 2px;
  height: 1.25em;
  margin-left: 1px;
  background: #4a9eff;
}

.kbdemo__placeholder {
  margin-left: 0.5rem;
  /* 4.92:1 on the screen's #0d0d0d. The grey this replaced looked right and
     measured 3.76:1, which is the usual way placeholder text fails. */
  color: #808080;
  font-size: 0.92rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.kbdemo__pills {
  display: flex;
  gap: 0.45rem;
  margin-top: 0.85rem;
  min-height: 2.9rem;
}

.kbdemo__pill {
  flex: 1 1 0;
  min-width: 0;
  min-height: 2.9rem;
  padding: 0.5rem 0.4rem;
  background: #3a3a3a;
  border: 1px solid #505050;
  border-radius: 8px;
  color: #e0e0e0;
  font-family: var(--font);
  font-size: 0.92rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}

@media (hover: hover) {
  .kbdemo__pill:hover {
    background: #4a9eff;
    border-color: #4a9eff;
    color: #05213f;
  }
}

/* Touch has no hover, so the press is the only feedback a tap gets in the
   moment before the word lands on the screen. Same colours as the hover above,
   and it cannot latch: :active ends when the finger lifts. */
.kbdemo__pill:active {
  background: #4a9eff;
  border-color: #4a9eff;
  color: #05213f;
}

/* A finger on a key should not select the letter printed on it, and a run of
   fast taps along a row should not start a selection that drags across the
   whole keyboard. iOS also raises a copy callout on a long press, which on a
   key is never what was meant. */
.kbdemo__key,
.kbdemo__pill {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

.kbdemo__keys {
  margin-top: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.kbdemo__row {
  display: flex;
  gap: 5px;
}

/* Every key clears the 24x24 CSS px of WCAG 2.5.8 with room to spare, and they
   stay square-ish rather than collapsing, because this is a demonstration of a
   keyboard built for imprecise pointing. */
.kbdemo__key {
  flex: 1 1 0;
  min-width: 0;
  min-height: 2.6rem;
  padding: 0.4rem 0.2rem;
  background: #3a3a3a;
  border: 1px solid #505050;
  border-radius: 7px;
  color: #e6e6e6;
  font-family: var(--font);
  font-size: clamp(0.72rem, 1.6vw, 0.95rem);
  font-weight: 600;
  text-transform: uppercase;
  cursor: pointer;
}

.kbdemo__key[data-pressed="true"],
.kbdemo__key:active {
  background: #5a5a5a;
  color: #fff;
}

@media (hover: hover) {
  .kbdemo__key:hover {
    background: #5a5a5a;
    color: #fff;
  }
}

.kbdemo__key--wide {
  flex: 1.6 1 0;
}

.kbdemo__key--space {
  flex: 1 1 100%;
  text-transform: lowercase;
  letter-spacing: 0.08em;
  color: #b8b8b8;
}

.kbdemo__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.9rem;
}

.kbdemo__saved {
  margin: 0;
  max-width: none;
  font-family: var(--font);
  font-size: 0.85rem;
  color: #9aa4b2;
}

.kbdemo__clear {
  flex: none;
  /* 2.75rem, not 2.5rem. The keys in this panel are allowed under 44px because
     ten of them will not fit across a phone at any larger size, and that trade
     is argued for in the media query below. Nothing forces this button to join
     them: it is one control on a row of its own, and it was 40px only because
     nobody had measured it. */
  min-height: 2.75rem;
  padding: 0.45rem 1rem;
  background: transparent;
  border: 1.5px solid #505050;
  border-radius: var(--radius-sm);
  color: #cfd6e0;
  font-family: var(--font);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}

@media (hover: hover) {
  .kbdemo__clear:hover {
    background: #2a2a2a;
    color: #fff;
    border-color: #6a6a6a;
  }
}

/* WCAG 2.5.8 asks for 24x24 CSS px per target. Ten keys plus their gaps, inside
   the page gutter and the panel's own padding, leaves 18px each at a 320px
   viewport, which is exactly the failure this product exists to avoid. Letting
   this one panel run to the screen edge and tightening its own padding buys the
   keys back to about 26px. The gutter is cancelled exactly rather than
   approximately, so the panel lines up with the viewport edge. */
@media (max-width: 560px) {
  .kbdemo {
    margin-left: calc(var(--gutter-left) * -1);
    margin-right: calc(var(--gutter-right) * -1);
    padding-block: 0.75rem;
    padding-left: max(0.5rem, env(safe-area-inset-left, 0px));
    padding-right: max(0.5rem, env(safe-area-inset-right, 0px));
    border-radius: 0;
    border-inline: 0;
  }

  .kbdemo__keys,
  .kbdemo__row {
    gap: 3px;
  }

  .kbdemo__pills {
    gap: 0.3rem;
  }
}

.kbdemo__hint {
  max-width: 46rem;
  margin: 1rem auto 0;
  font-size: 0.88rem;
  /* NOT --ink-faint: the inline <code> here sits on --surface-2 rather than on
     the page background, and --ink-faint measures 4.20:1 against it. */
  color: var(--ink-muted);
}

.kbdemo__hint code {
  background: var(--surface-2);
}

/* ---------- Feature grid ---------- */

.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17.5rem, 100%), 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
}

.feature {
  padding: 1.6rem 1.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.16s ease, transform 0.16s ease;
}

@media (hover: hover) {
  .feature:hover {
    border-color: var(--border-strong);
    transform: translateY(-3px);
  }
}

.feature__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: 1.05rem;
  border-radius: 10px;
  background: var(--accent-wash);
  color: var(--accent);
}

.feature__icon svg {
  width: 1.4rem;
  height: 1.4rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.feature h3 {
  margin-bottom: 0.5rem;
}

.feature p {
  margin-bottom: 0;
  color: var(--ink-muted);
  font-size: 0.97rem;
}

/* ---------- Screenshot gallery ---------- */

.shots {
  display: grid;
  /* 26rem rather than 20rem: at the 1180px page width 20rem yields three
     columns, which shrinks a settings screenshot below the point where its own
     labels are readable. Two columns is the useful maximum here. */
  grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr));
  gap: clamp(1.25rem, 2.5vw, 2rem);
}

.shot {
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.shot--wide {
  grid-column: 1 / -1;
}

.shot img {
  width: 100%;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

/* The snippets shot is a narrow floating window; letting it stretch to the
   card width blows it up to twice its natural size and it reads as blurry. */
.shot--narrow img {
  max-width: 340px;
  margin-inline: auto;
  border-bottom: 0;
  padding: 1.5rem 0 0;
}

.shot figcaption {
  padding: 1.1rem 1.35rem 1.35rem;
  color: var(--ink-muted);
  font-size: 0.95rem;
}

.shot figcaption strong {
  display: block;
  color: var(--ink);
  font-weight: 650;
  margin-bottom: 0.2rem;
}

/* ---------- Download ---------- */

.downloads {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17.5rem, 100%), 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-bottom: 2rem;
}

.dl {
  display: flex;
  flex-direction: column;
  padding: 1.75rem 1.6rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.dl--primary {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.dl__os {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 700;
  font-size: 1.15rem;
  margin-bottom: 0.4rem;

  /* The platform name is an anonymous flex item beside its logo, so it takes
     the same treatment as the FAQ questions: without it "Windows" is floored
     at its own 150px and carries the whole download grid three pixels off the
     right of a 320px screen at a doubled root. anywhere only ever breaks a
     word that has no other way to fit, which at that size beats the
     alternative of a page that scrolls sideways. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.dl__os svg {
  width: 1.35rem;
  height: 1.35rem;
  fill: currentColor;
  flex: none;
}

.dl__meta {
  color: var(--ink-muted);
  font-size: 0.93rem;
  margin-bottom: 1.3rem;
  flex: 1;
}

.dl .btn {
  width: 100%;
}

.dl__status {
  display: inline-flex;
  align-items: center;
  min-height: 3.4rem;
  padding: 0.85rem 1.2rem;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--ink-faint);
  font-weight: 600;
  justify-content: center;
}

.callout {
  display: flex;
  gap: 1rem;
  padding: 1.35rem 1.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius-sm);
}

.callout svg {
  flex: none;
  width: 1.3rem;
  height: 1.3rem;
  margin-top: 0.2rem;
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.callout p {
  margin: 0;
  font-size: 0.96rem;
  color: var(--ink-muted);

  /* A flex item will not shrink below its longest word unless it is told it
     may. Beside a 1.3rem icon and a 1rem gap on a 320px screen, at a doubled
     default font size, "Dictation" and "Windows" were each wider than the
     column left over, so both callouts pushed themselves past the right edge
     of the page. */
  min-width: 0;
}

.callout strong {
  color: var(--ink);
}

/* ---------- FAQ ---------- */

.faq {
  max-width: 52rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}

.faq details + details {
  border-top: 1px solid var(--border);
}

.faq summary {
  display: flex;
  align-items: center;
  /* No gap. The chevron is pushed to the right edge by its own margin-left:
     auto, which absorbs whatever space is going, so a gap beside it is only
     ever felt in the one case where there is no space to give: a long question
     on a narrow screen at a large font size, where it was reserving 1rem that
     the question needed and pushing the text past the panel. */
  min-height: 3.9rem;
  padding: 1rem 1.4rem;
  font-weight: 650;
  font-size: 1.02rem;
  cursor: pointer;
  list-style: none;
  color: var(--ink);

  /* anywhere, not break-word, and the difference is the whole point: only
     anywhere is taken into account when the browser works out a min-content
     width. The question text is an anonymous flex item beside the chevron, so
     there is no element to hand a min-width: 0 to, and the item was floored at
     the width of "keystrokes": 224px inside a 238px panel at a doubled root,
     which pushed the longest question out of the box. Neither value breaks a
     word while a normal wrap opportunity is still available, so nothing about
     these questions changes at any ordinary size. */
  overflow-wrap: anywhere;
}

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

@media (hover: hover) {
  .faq summary:hover {
    background: var(--surface-2);
  }
}

.faq summary::after {
  content: "";
  flex: none;
  margin-left: auto;
  width: 0.6rem;
  height: 0.6rem;
  border-right: 2.5px solid var(--ink-faint);
  border-bottom: 2.5px solid var(--ink-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.18s ease;
}

.faq details[open] summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.faq details[open] summary {
  color: var(--accent);
}

.faq__body {
  padding: 0 1.4rem 1.4rem;
  color: var(--ink-muted);
}

.faq__body p:last-child {
  margin-bottom: 0;
}

/* ---------- Footer ---------- */

.site-footer {
  border-top: 1px solid var(--border);
  background: var(--bg-alt);
  /* The home indicator sits over the last 34px of the viewport on a phone
     without a home button, which is exactly where the copyright line was. */
  padding-block: clamp(2.5rem, 5vw, 4rem) calc(2rem + env(safe-area-inset-bottom, 0px));
}

/* Two grids, not one. The outer grid decides whether the about block sits
   beside the links or above them; the inner grid decides how many columns the
   links use. Those are separate questions and they now have separate answers,
   which is the shape atdev.com's footer already uses: .footer__inner holding
   a .footer__cols of its own.

   What was here was a single flat four-track row, and its template read
   minmax(0, 1.4fr) followed by repeat(auto-fit, minmax(min(9rem, 100%), 1fr)).
   Those two halves fight each other. auto-fit counts how many 9rem tracks to
   make by measuring the whole row, and the leading track's minimum is 0, so
   the link columns take what they want and the about column lives on whatever
   is left. At 600px what was left was 24 CSS px: the paragraph set itself one
   word per line, the brand overlapped the first heading, and the footer stood
   897px tall, through a band of widths that covers small tablets and any large
   phone held sideways.

   Nesting removes the interaction rather than tuning it, because no row now
   contains both an fr track and a fitted one. 1.4fr against 3fr keeps the
   desktop proportions the flat version had to within about 20px: the inner
   gaps now sit inside the 3fr track rather than beside it, which moves the
   split by that much and by nothing anyone will see. */
.footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 3fr);
  gap: 2rem;
  margin-bottom: 2.5rem;
}

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

/* Under the width the two of them share comfortably, the about block takes the
   row above. Its paragraph is still capped at 30rem by .footer-about p, so it
   keeps a readable measure rather than running the full width of a tablet. */
@media (max-width: 860px) {
  .footer-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 2.25rem;
  }

  .footer-links {
    gap: 1.75rem 1.25rem;
  }
}

/* The widest thing in a link column is the word "Documents", at 95px with
   Inter loaded and 91px in the fallback stack, so three columns and their two
   gaps stop fitting at around a 365px viewport. Two columns leave the last
   group without a neighbour, which reads as a gap; three columns of wrapped
   link text reads as a fault.

   In em rather than px, and the only query in this file that is. All the
   others answer "how much room is there", which is a question about the
   device. This one answers "does this text still fit", and text is not a fixed
   number of pixels: a visitor who has raised their default font size or zoomed
   in gets wider words in the same viewport, and a px query would hold three
   columns while the words outgrew them. In em the threshold rises with the
   text, which is when the extra room is needed. 23.5em is 376px at a default
   root, so a 390px phone keeps three columns and a 375px one drops to two. */
@media (max-width: 23.5em) {
  .footer-links {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* And one column when two will not hold the longest word either. The step
   above is the whole answer at any ordinary font size, but at the 200% that
   1.4.4 asks for, "Contributing" is 5.6em wide and a half-column on a 390px
   phone is 4.2em: the word ran out of its track, out of the footer and off the
   page, which is a horizontal scrollbar on the one document width that is
   supposed to be reachable without one.

   15em is 240px at a default root, narrower than the 320px this site claims to
   support, so this never fires for a reader who has not enlarged anything. At
   a doubled root it is 480px, which is every phone. Same reasoning as the query
   above, one step further along: the threshold is a fact about the word. */
@media (max-width: 15em) {
  .footer-links {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.25rem;
  }
}

.footer-about p {
  color: var(--ink-muted);
  font-size: 0.93rem;
  margin: 0.9rem 0 0;
  max-width: 30rem;
}

.footer-col h2 {
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.5rem;
}

.footer-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* 2.75rem, not 2.5rem. The comment at the top of this file says every
   interactive target on the site clears 44x44 CSS px, and these twelve were
   40px tall: the one place on a phone where a thumb is most likely to be
   reaching, and the only place the claim was not true. */
/* The target is worth the whole column rather than the width of the word:
   inline-flex sizes to the text, which on "FAQ" is 29px of hittable width in a
   160px column. This was a touch-only rule under an 860px query, which left
   that 29px target on every desktop and was the last thing on the page under
   the 44px the top of this file claims. Nothing here changes with the width,
   so it does not need a query: the text stays where it was, and only the box
   around it grows. */
.footer-col a {
  display: flex;
  align-items: center;
  min-height: 2.75rem;
  color: var(--ink-muted);
  font-size: 0.95rem;
  text-decoration: none;
}

@media (hover: hover) {
  .footer-col a:hover {
    color: var(--accent);
    text-decoration: underline;
  }
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  padding-top: 1.75rem;
  border-top: 1px solid var(--border);
  color: var(--ink-faint);
  font-size: 0.88rem;
}

.footer-bottom p {
  margin: 0;
}

/* The 404 page's "Report a problem" is the only link down here, and as plain
   inline text it was a 17px-tall target sitting on its own line rather than
   inside a sentence. Nothing in index.html matches this. */
.footer-bottom a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
}

/* ---------- 404 ---------- */

.error-page {
  /* dvh rather than vh so the page is not measured against a viewport that
     includes the browser toolbars a phone hides as soon as you scroll. */
  min-height: 70vh;
  min-height: 70dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.error-page__code {
  font-family: var(--mono);
  font-size: clamp(4rem, 12vw, 8rem);
  font-weight: 800;
  line-height: 1;
  color: var(--accent);
  margin-bottom: 0.5rem;
}

/* ---------- Forced colours (Windows High Contrast) ---------- */

/* The OS replaces every colour here with its own palette, and in doing so drops
   background images, gradients and box-shadows entirely. Anything on this page
   that carries meaning through one of those three has to say it a second way,
   or the page arrives as undifferentiated text on a flat ground. This matters
   more than usual for this site: high contrast mode is exactly what a good part
   of the audience for an on-screen keyboard is running. */
@media (forced-colors: active) {
  /* .stat cards are separated by a background change, which is gone. Give
     them edges. Only this one: forced colours recolour borders but do not
     remove them, so every other card in this stylesheet already declares one
     and already keeps a visible edge here. Naming them anyway was worse than
     doing nothing, because the border shorthand resets all four sides: it
     flattened .callout's 4px accent rail to 1px and thinned .btn from 1.5px,
     in the one mode where the user has asked for maximum contrast. */
  .stat {
    border: 1px solid;
  }

  /* The recommended download is marked only by a box-shadow ring, which forced
     colours discard, so the card would lose its emphasis completely. Doubled
     rather than solid: :focus-visible below draws a 3px solid ring at a 3px
     offset, and a permanent ring in the same vocabulary as the focus ring left
     a keyboard user unable to tell which card they were actually on. */
  .dl--primary {
    outline: 3px double;
    outline-offset: 2px;
  }

  /* The same defect one level up, and the reason the rule above exists. A
     primary button differs from a secondary one only by background colour,
     which is flattened to a single system colour here, so all three Download
     calls to action became indistinguishable from "View the source". Width is
     preserved by forced colours where colour is not. */
  .btn--primary {
    border-width: 3px;
  }

  /* Key presses and pill hovers in the demo panel say what they mean through
     background-color alone, which is exactly what this mode overrides: without
     a second channel, pressing a key on a physical keyboard flashed nothing on
     its on-screen twin and hovering a suggestion did nothing at all. */
  .kbdemo__key[data-pressed="true"],
  .kbdemo__key:active,
  .kbdemo__pill:active {
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  /* Decorative gradients paint nothing useful here and can leave a stray block
     of system colour behind the text. */
  .hero::before,
  .hero::after {
    display: none;
  }

  /* Let the system decide the focus colour rather than forcing our accent,
     which forced-colors would override to an arbitrary member of the palette. */
  :focus-visible {
    outline: 3px solid;
    outline-offset: 3px;
  }
}

/* The hover half of the rule above, kept out of the block so that a phone in
   forced colours does not leave the last key it touched underlined. */
@media (forced-colors: active) and (hover: hover) {
  .kbdemo__key:hover,
  .kbdemo__pill:hover {
    text-decoration: underline;
    text-underline-offset: 3px;
  }
}

/* ---------- Motion ---------- */

@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;
  }

  /* The rule above stops an animation by running it to its end in a millisecond,
     which for the hero auras would leave them parked on their "to" keyframe:
     a composition chosen for the far end of a drift rather than for standing
     still. none leaves them where the CSS put them, which is the arrangement
     everything else here was measured against. */
  .hero::before,
  .hero::after {
    animation: none !important;
  }
}

/* ---------- Print ---------- */

@media print {
  .site-header,
  .hero__cta,
  .theme-toggle {
    display: none;
  }

  /* Large flat areas of pale colour, at the top of the first page, in ink. */
  .hero::before,
  .hero::after {
    display: none;
  }

  body {
    background: #fff;
    color: #000;
  }
}

/* ---------------------------------------------------------------------------
   Deep-dive links
   ---------------------------------------------------------------------------
   A row of outbound links to the app repo's design documents, sitting at the
   foot of a section. The label is a span and not a heading: it names the row
   for someone reading down the page, and in the heading outline it would be a
   fourth level under a section that already has an h2 and a set of h3 cards.

   The arrow is a ::after on the link rather than a character in the markup so
   that it cannot be selected, copied or read out as the word "north east
   arrow" by a screen reader. Every link in one of these rows leaves the site,
   which is the whole reason the row is visually distinct from body copy. */
.deep-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.55rem 1.6rem;
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  margin-bottom: 0;
}

.deep-links__label {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* 44px, like everything else that is not a key in the demo panel. These are
   flex items rather than links inside a sentence, so 2.5.8 has no inline
   exception to give them and they were 20px: the smallest targets on the page,
   on a site whose argument is that small targets are the problem. inline-flex
   makes the height reachable without the row growing, since the label they sit
   beside is centred against them rather than baselined to them. */
.deep-links a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  font-size: 0.95rem;
  font-weight: 600;
}

/* A "read the rest of it" link alone in its own paragraph. Same 20px problem
   and the same fix: alone in a block, it is not a link in a sentence, so it
   has to carry its own target height rather than inherit a line box. */
.more-link {
  margin-top: 2rem;
  margin-bottom: 0;
}

.more-link a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  font-weight: 600;
}

.deep-links a::after {
  content: "\2197";
  display: inline-block;
  margin-left: 0.3em;
  font-weight: 400;
  /* The arrow rides high next to a 600-weight label at this size; a hair of
     translation seats it on the same optical line as the text. */
  transform: translateY(0.05em);
}

/* ---------------------------------------------------------------------------
   Study page
   ---------------------------------------------------------------------------
   One addition. study.html reuses .features, .privacy, .downloads/.dl,
   .callout, .badge and .btn as they already stand; the only thing none of
   them already said is "this is the one the study is actually asking about",
   which the recommended Windows download card already answers for itself
   with an accent border and ring. Repeating that here, under its own name,
   is cheaper than teaching a fifth card style what "highlighted" means. */
.feature--headline {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* ---------------------------------------------------------------------------
   Forms
   ---------------------------------------------------------------------------
   One form on the site: the study interest form. The controls borrow the card
   surfaces rather than introducing their own, so a field sits on --surface-2
   the same way a key or a code sample does, and the focus ring is the shared
   one from the top of this file rather than a second style that has to be kept
   in step with it. */
.form {
  display: grid;
  gap: 1.1rem;
  /* The form is the last thing in a .dl card, which sets its own bottom
     padding; without this the note below the button doubles it. */
  margin: 0;
}

.field {
  display: grid;
  gap: 0.4rem;
}

.field label {
  font-size: 0.93rem;
  font-weight: 600;
  color: var(--ink);
}

/* "Optional" has to read as quieter than the label without dropping under
   4.5:1, which is what --ink-muted is picked for. */
.field__opt {
  font-weight: 400;
  color: var(--ink-muted);
}

.field__hint {
  font-size: 0.85rem;
  color: var(--ink-muted);
}

.form input[type="email"],
.form input[type="text"],
.form textarea {
  width: 100%;
  padding: 0.7rem 0.85rem;
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 1rem;
  /* Below 16px iOS zooms the page on focus and does not zoom back out. */
}

.form textarea {
  resize: vertical;
  min-height: 5.5rem;
}

.form ::placeholder {
  color: var(--ink-faint);
  opacity: 1;
}

.form input:hover,
.form textarea:hover {
  border-color: var(--accent);
}

/* Only after a submit attempt: :invalid on its own paints every empty required
   field red before the reader has typed anything. */
.form input:user-invalid,
.form textarea:user-invalid {
  border-color: var(--warn);
}

.form button[type="submit"] {
  justify-self: start;
}

.form__note {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-muted);
}

/* The honeypot. Not display:none and not [hidden]: a bot that skips hidden
   inputs would step around the trap, so it stays in the layout and is moved
   out of view instead. aria-hidden and tabindex="-1" in the markup keep it
   away from screen readers and the tab order. */
.form__hp {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
