/* MumbleWay — a night ride, an instrument cluster, a visor at dusk.
   Committed to one dark world on purpose: the app is dark, it is read at
   night and in gloves, and a second theme here would be a theme nobody asked
   for rather than a courtesy. */

:root {
  --ground:   #0A0E13;
  --surface:  #141B24;
  --raised:   #1C2530;
  --line:     #263140;
  --line-soft:#1B242F;

  --text:     #DDE6EF;
  --muted:    #8695A6;
  --dim:      #61707F;

  /* Sodium vapour. The colour of the road at night and of the app's own
     latency readout — chosen from the subject rather than from a palette. */
  --accent:   #FFA53C;
  --accent-d: #C97A1E;
  --signal:   #3ECF7E;

  --measure: 68ch;
  --pad: clamp(1.25rem, 4vw, 2.5rem);

  --sans: "Atkinson Hyperlegible", ui-sans-serif, system-ui, sans-serif;
  --display: "Exo 2", var(--sans);
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

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

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

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--sans);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.65;
  /* `clip`, not `hidden`. They look identical and are not: `hidden` makes this
     a scroll container, and a sticky element inside a scroll container sticks
     to *that* box, which scrolls away with the document — so the top bar
     silently stopped sticking. `clip` cuts the overflow without creating a
     scroller, and leaves the bar stuck to the viewport where it belongs.
     Wide content is not relying on either: tables, code blocks and figures
     each scroll inside their own container. */
  overflow-x: clip;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: #17110A;
  padding: 0.6rem 1rem;
  z-index: 100;
}
.skip:focus { left: 0; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { color: #FFC276; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }

/* ---------- top bar ---------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.5rem;
  padding: 0.7rem var(--pad);
  background: color-mix(in srgb, var(--ground) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line-soft);
}

/* The app's own lockup rather than the name set in type. It is two lines with
   the second hung off to the right, so it wants height rather than width —
   which is exactly what a bar has spare, and is why the app draws it that way
   in its own toolbar. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  text-decoration: none;
}
.brand-logo {
  height: 2.1rem;
  width: auto;
  display: block;
}
/* Sits on the wordmark's baseline rather than the box's centre: the lockup is
   optically bottom-heavy, and centring the dot against the whole image leaves
   it floating above the word it belongs to. */
.brand-mark {
  width: 0.7rem; height: 0.7rem;
  flex: none;
  align-self: center;
  border-radius: 50%;
  background: var(--signal);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--signal) 22%, transparent);
}

.topbar nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.1rem;
  margin-left: auto;
  font-size: 0.925rem;
}
.topbar nav a {
  color: var(--muted);
  text-decoration: none;
  padding: 0.2rem 0;
  border-bottom: 2px solid transparent;
}
.topbar nav a:hover { color: var(--text); }
.topbar nav a[aria-current="page"] { color: var(--text); border-bottom-color: var(--accent); }

/* ---------- the narrow-screen menu ---------- */

.menu-button {
  display: none;
  margin-left: auto;
  width: 2.75rem;
  height: 2.75rem;   /* A thumb target, not an icon's bounding box. */
  padding: 0;
  background: none;
  border: 1px solid var(--line);
  color: var(--text);
  cursor: pointer;
  align-items: center;
  justify-content: center;
}
.menu-button:hover { border-color: var(--dim); }

/* Three rules drawn from one element: the middle is the box, the other two are
   its pseudo-elements. Fewer nodes, and the whole thing animates as one. */
.bars, .bars::before, .bars::after {
  display: block;
  width: 1.15rem;
  height: 2px;
  background: currentColor;
  transition: transform .18s ease, opacity .18s ease;
}
.bars { position: relative; }
.bars::before, .bars::after { content: ""; position: absolute; left: 0; }
.bars::before { top: -6px; }
.bars::after  { top: 6px; }

/* Open, it becomes a cross — the same three rules, rearranged, so the control
   says what a second press will do. */
.topbar.open .bars { background: transparent; }
.topbar.open .bars::before { transform: translateY(6px) rotate(45deg); }
.topbar.open .bars::after  { transform: translateY(-6px) rotate(-45deg); }

/* Measured, not guessed.
   A pixel breakpoint has to know how wide the labels are, and it cannot: the
   set of links changes as pages are added, and every one of them changes
   length again in another language. The first version was set for six items,
   a seventh was added, and the row quietly began wrapping to two lines in a
   band of widths nobody had thought to look at.
   So the script measures whether the row fits and sets `.compact` when it does
   not. There is no width in this file and no breakpoint to keep in step.

   There was a `.topbar { position: relative }` here, to give the dropdown
   below something to be absolute against. It silently un-stuck the bar: this
   rule comes after the one that sets `position: sticky`, so it won, and the
   bar scrolled away for months without anyone connecting the two. Nothing
   needs to replace it — `sticky` is a positioned value and is already the
   containing block the dropdown wants. */

.topbar.compact .menu-button { display: inline-flex; }
.topbar.compact nav {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  flex-direction: column;
  gap: 0;
  padding: 0.5rem var(--pad) 1rem;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  box-shadow: 0 18px 30px -18px #000;
}
.topbar.compact.open nav { display: flex; }
.topbar.compact nav a {
  padding: 0.75rem 0;
  font-size: 1.02rem;
  border-bottom: 1px solid var(--line-soft);
}
.topbar.compact nav a:last-child { border-bottom: 0; }
/* The underline marking the current page is a bottom border on a row that
   already has one, so it moves to the side where it reads as a marker. */
.topbar.compact nav a[aria-current="page"] {
  border-bottom-color: var(--line-soft);
  box-shadow: inset 3px 0 0 var(--accent);
  padding-left: 0.7rem;
}

/* ---------- language switch ---------- */

/* The app's own language button, matched: no frame, an 8px corner radius,
   10/8 padding, a 14px flag, a 6px gap and the code at 13px/700 with 0.4px of
   letter-spacing. The numbers are pixels rather than rems on purpose — they
   are copied from `app/lib/widgets/language_button.dart`, where they are
   logical pixels, and converting them to a relative unit would be a different
   control that merely resembled it.

   Visible at every width, which is the whole requirement: it is never behind
   the hamburger, because somebody who cannot read the page cannot be asked to
   find a menu labelled in a language they do not read. */
.lang {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  padding: 8px 10px;
  border-radius: 8px;
  color: var(--text);
  text-decoration: none;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.4px;
  line-height: 1;
  white-space: nowrap;
  transition: background-color .15s;
}
/* Standing in for the ripple, which is the one thing here a link cannot have. */
.lang:hover { background: var(--raised); }
.lang img {
  display: block;
  height: 14px;
  /* Width follows: the flags have different proportions and squashing either
     one to a fixed box is exactly what the app avoids with BoxFit.contain. */
  width: auto;
  border-radius: 2px;
}

/* Right of the menu when the menu is a row, left of the button when it is not.
   Source order puts it last, which is the wide case; compact reorders rather
   than duplicating the element, so there is one switch on the page and one
   tab stop for it.

   In compact the nav is taken out of flow by `position: absolute`, so the
   three items left in the row are the brand, this, and the button. Moving the
   button to the end and handing the automatic margin to the switch puts them
   in that order without either one knowing a width. */
.topbar.compact .menu-button { order: 1; margin-left: 0; }
.topbar.compact .lang { margin-left: auto; }

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

.hero {
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--line-soft);
  background:
    radial-gradient(120% 90% at 12% -20%, #16202C 0%, transparent 60%),
    var(--ground);
}

/* Behind the words and staying there. At full strength the bars reach into the
   headline and the type has to compete with a moving background, which is the
   failure mode of every animated hero: legibility lost to decoration. It sits
   in the lower half, at a third strength, fading out well before the text. */
#analyser {
  position: absolute;
  inset: auto 0 0 0;
  width: 100%;
  height: 46%;
  opacity: 0.34;
  mask-image: linear-gradient(to top, #000 0%, #000 22%, transparent 92%);
  -webkit-mask-image: linear-gradient(to top, #000 0%, #000 22%, transparent 92%);
}
/* A scrim between the bars and the words, so the lede stays readable over the
   busiest part of the animation without dimming the bars themselves. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to bottom,
    var(--ground) 0%,
    color-mix(in srgb, var(--ground) 55%, transparent) 42%,
    transparent 72%);
}
.hero-inner {
  position: relative;
  z-index: 1;
  max-width: 62rem;
  margin: 0 auto;
  padding: clamp(3.5rem, 9vw, 7rem) var(--pad) clamp(4rem, 10vw, 8rem);
}

.eyebrow {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 1.4rem;
}

.hero h1 {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(2.6rem, 8vw, 5.2rem);
  line-height: 0.98;
  letter-spacing: -0.035em;
  margin: 0 0 1.2rem;
  text-wrap: balance;
}

.lede {
  font-size: clamp(1.08rem, 1rem + 0.5vw, 1.3rem);
  color: var(--muted);
  max-width: 46ch;
  margin: 0 0 2rem;
}
/* Half again as much in the hero, which is the space between everything the
   page says about itself and the row that acts on it. At 2rem the badges read
   as another paragraph of the introduction; at 3rem they are the next thing,
   without the row drifting far enough from the words to look unrelated to
   them. Scoped to the hero on purpose — the same class introduces every other
   page, where there is nothing below it and the gap would just be a hole. */
.hero .lede { margin-bottom: 3rem; }

/* Two gaps, not one. Along the row 0.75rem is right — it is the spacing between
   controls that belong together. Between rows it is not: the row below is the
   store badges, a different kind of thing from the two buttons above, and at
   0.75rem they read as one block of five controls that happened to wrap.

   The row gap is a whole control's height, which is why it is `--cta-h` and not
   a number: it is the same measure the buttons and badges are built from, so
   the empty row between the two groups is exactly as tall as the things either
   side of it and stays that way as the type scales with the window. */
.cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 0.75rem;
  /* Half, because the rule between the two groups is itself a row: the gap is
     paid twice, above it and below it, and the two groups still end up a
     control's height apart. */
  row-gap: calc(var(--cta-h) / 2);
  margin-bottom: 2.5rem;
}
.btn {
  display: inline-block;
  padding: 0.7rem 1.4rem;
  border: 1px solid var(--line);
  color: var(--text);
  text-decoration: none;
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: 0.01em;
  background: var(--surface);
  transition: border-color .15s, background .15s, color .15s;
}
.btn:hover { border-color: var(--dim); color: var(--text); }
.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #17110A;
}
.btn.primary:hover { background: #FFB761; border-color: #FFB761; color: #17110A; }

/* This line sits directly on top of the bars it describes, which is the point
   and also the problem: at the dim grey used elsewhere it was being read
   through a moving background. Lifted a step and given a shadow the colour of
   the ground, so it stays legible over a bar and over a gap between two. */
.hero-note {
  font-size: 0.88rem;
  color: var(--muted);
  max-width: 44ch;
  margin: 0;
  text-shadow: 0 1px 6px var(--ground), 0 0 14px var(--ground);
}

/* ---------- page head ---------- */

.pagehead {
  max-width: 62rem;
  margin: 0 auto;
  padding: clamp(2.5rem, 6vw, 4.5rem) var(--pad) 0;
}
.pagehead h1 {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(2rem, 5.5vw, 3.4rem);
  line-height: 1.03;
  letter-spacing: -0.03em;
  margin: 0 0 0.8rem;
  text-wrap: balance;
}

/* ---------- prose ---------- */

.prose {
  max-width: 62rem;
  margin: 0 auto;
  padding: clamp(2.5rem, 6vw, 4rem) var(--pad) 5rem;
}
.prose > * { max-width: var(--measure); }

.prose h2 {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(1.5rem, 3vw, 2rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 3.5rem 0 1rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--line-soft);
  max-width: 100%;
  text-wrap: balance;
}
.prose > h2:first-child { margin-top: 0; border-top: 0; padding-top: 0; }

.prose h3 {
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.18rem;
  letter-spacing: -0.01em;
  margin: 2.2rem 0 0.5rem;
}

.prose p, .prose ul, .prose ol { margin: 0 0 1.15rem; }
.prose li { margin-bottom: 0.4rem; }
.prose strong { color: #F3F7FB; font-weight: 700; }

.prose blockquote {
  margin: 1.6rem 0;
  padding: 0.2rem 0 0.2rem 1.2rem;
  border-left: 2px solid var(--accent-d);
  color: var(--muted);
  font-style: italic;
}

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

/* ---------- code ---------- */

code {
  font-family: var(--mono);
  font-size: 0.875em;
  background: var(--raised);
  border: 1px solid var(--line-soft);
  padding: 0.1em 0.36em;
  border-radius: 3px;
}
pre {
  max-width: 100%;
  overflow-x: auto;
  background: #0E141B;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent-d);
  padding: 1rem 1.15rem;
  margin: 0 0 1.4rem;
  line-height: 1.55;
}
pre code {
  background: none;
  border: 0;
  padding: 0;
  font-size: 0.84rem;
  color: #C9D6E4;
}

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

.table-wrap { overflow-x: auto; margin: 0 0 1.6rem; max-width: 100%; }
table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.94rem;
}
th, td {
  text-align: left;
  padding: 0.6rem 0.9rem 0.6rem 0;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
th {
  font-family: var(--display);
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  border-bottom-color: var(--line);
  white-space: nowrap;
}
td:first-child { color: #F3F7FB; font-weight: 700; padding-right: 1.4rem; }
tbody tr:last-child td { border-bottom: 0; }

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

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  padding: 1.3rem 1.5rem;
  margin: 0 0 1.4rem;
  max-width: 100%;
}
.panel h3 { margin-top: 0; }
.panel > :last-child { margin-bottom: 0; }

.panel.warn { border-left: 3px solid var(--accent); }
.panel.good { border-left: 3px solid var(--signal); }

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  max-width: 100%;
  margin: 0 0 1.6rem;
}
.grid .panel { margin: 0; }

/* Specs read as instruments: label small and quiet, value large and lit.
   The mono is on the label alone — set on the panel it inherits into the
   sentence underneath, and a paragraph of prose in a monospace face reads as a
   terminal rather than as writing. */
.spec .k {
  display: block;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 0.3rem;
}
.spec .v {
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.5rem;
  color: var(--accent);
  letter-spacing: -0.02em;
}

.pill {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.18rem 0.5rem;
  border: 1px solid var(--line);
  color: var(--muted);
  margin-right: 0.35rem;
}

.muted { color: var(--muted); }

/* ---------- figures ---------- */

/* Wider than the reading measure. A diagram read at 68ch is a diagram with its
   labels squeezed, and these earn the extra width by carrying something the
   prose beside them cannot say in a sentence. */
.figure {
  margin: 2rem 0 2.5rem;
  max-width: 100%;
}
.fig {
  display: block;
  width: 100%;
  height: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  padding: 1.25rem;
}
.figure figcaption {
  margin-top: 0.85rem;
  font-size: 0.9rem;
  color: var(--muted);
  max-width: var(--measure);
}

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

/* One width for every screenshot, whatever it is a picture of.
   A phone capture is tall and narrow, a desktop window is short and wide, and
   sizing each to its own shape made a page of screenshots look like a page of
   accidents — the desktop ones ran the full measure while the phones sat in a
   thumbnail row beside them. They are all the same *object* to a reader: a
   picture of the app. So they are all one width, their height follows their
   aspect ratio, and the detail lives in the lightbox a click away.

   That trade is only honest because the click exists. A 1000px window at 16rem
   is unreadable, and a caption saying what it shows is not a substitute for
   being able to look. */
:root { --shot-w: 16rem; }

.shots {
  display: flex;
  flex-wrap: wrap;
  /* Pictures aligned at the top. Aligning the bottoms instead did line the
     captions up, but it bought that by leaving the tops ragged — which is the
     edge a reader actually follows across a row. */
  align-items: flex-start;
  gap: 1.25rem;
  margin: 2rem 0 2.5rem;
  max-width: 100%;
}
.shots figure { margin: 0; flex: 0 0 auto; max-width: 100%; }

/* Captions aligned separately from the pictures.
   One flex line can only align a figure by one edge, so tops and captions are
   a choice between two. Two grid rows are not: every picture starts on the
   first line and every caption on the second, whatever the pictures' heights.
   The flex rules above stay as the fallback, so a browser without subgrid gets
   top-aligned pictures and ragged captions rather than a broken row. */
@supports (grid-template-rows: subgrid) {
  .shots {
    display: grid;
    grid-template-columns:
      repeat(auto-fit, minmax(min(var(--shot-w), 100%), var(--shot-w)));
    grid-template-rows: auto auto;
    justify-content: start;
    align-items: start;
  }
  .shots figure {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
  }
}

.shots img {
  display: block;
  width: var(--shot-w);
  max-width: 100%;
  height: auto;
  border: 1px solid var(--line);
  background: var(--ground);
}

.shots figcaption {
  margin-top: 0.6rem;
  font-size: 0.85rem;
  color: var(--muted);
  /* Tied to the picture rather than to the measure: a caption wider than the
     thing it captions reads as body copy that has lost its paragraph. */
  max-width: var(--shot-w);
}

/* ---------- lightbox ---------- */

/* The link is what makes this work without JavaScript: it points at the image
   file, so a reader with no script still gets the full-size picture, just in a
   tab of its own. The script intercepts the click and shows it in place. */
.shot-link {
  display: block;
  width: max-content;
  max-width: 100%;
  cursor: zoom-in;
  line-height: 0;
}
.shot-link img { transition: border-color .15s; }
.shot-link:hover img,
.shot-link:focus-visible img { border-color: var(--accent); }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: color-mix(in srgb, #000 86%, transparent);
  backdrop-filter: blur(4px);
  cursor: zoom-out;
}
.lightbox img {
  /* Never larger than it really is: upscaling a 560px capture to fill a 4K
     display shows the reader nothing except the interpolation. */
  max-width: min(100%, 1000px);
  /* Viewport units rather than a percentage. A percentage resolves against the
     figure, whose height is auto, so it never constrained anything and a tall
     phone capture ran off the bottom of the screen with its caption. The
     subtraction is the overlay's own padding plus room for the caption. */
  max-height: calc(100vh - 9rem);
  width: auto;
  height: auto;
  border: 1px solid var(--line);
  background: var(--ground);
  box-shadow: 0 24px 60px -20px #000;
  cursor: default;
}
.lightbox figcaption {
  margin-top: 0.8rem;
  font-size: 0.9rem;
  color: var(--muted);
  text-align: center;
  max-width: 46ch;
}
.lightbox figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  max-height: 100%;
}
.lightbox-close {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  width: 2.75rem;
  height: 2.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
}
.lightbox-close:hover { border-color: var(--dim); }

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

.footer {
  border-top: 1px solid var(--line-soft);
  padding: 3rem var(--pad) 4rem;
  background: #080B0F;
}
.footer-grid {
  max-width: 62rem;
  margin: 0 auto 2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
  justify-content: space-between;
}
.footer-logo {
  height: 2.6rem;
  width: auto;
  display: block;
  margin: 0 0 0.7rem;
}
.footer p { margin: 0; font-size: 0.9rem; }
.footer nav { display: flex; flex-direction: column; gap: 0.4rem; font-size: 0.9rem; }
.footer nav a { color: var(--muted); text-decoration: none; }
.footer nav a:hover { color: var(--text); }
.colophon {
  max-width: 62rem;
  margin: 0 auto;
  color: var(--dim);
  font-size: 0.82rem;
  max-width: 52ch;
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* The store badges in the hero, above a rule, with the two buttons under it.
   Separated because they answer different questions: the badges are "give it to
   me", the buttons are "how does this work". */
/* One flex item, so the three badges stay together on their line and only
   break up badge by badge when the group alone is wider than the row. Three
   stores going down one at a time while the row still has space for two of
   them reads as three unrelated things rather than one set. */
.stores {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
}
/* The vendors' badges are used as supplied and scaled by height only, which is
   what all three sets of brand rules ask for. Their aspect ratios differ, so
   fixing the height and letting width follow keeps every one of them correct
   without a per-store size.

   The height is the button's beside them, restated rather than measured:
   line box + padding + border, in the same units the button declares them in.
   A fixed pixel height cannot work here, because `.btn` takes its font size
   from a `clamp()` with a `vw` term and therefore grows with the window — so
   the two would agree at one width and nowhere else. Each term below mirrors
   one declaration: 1.65 is the inherited `line-height`, 1.4rem the two 0.7rem
   paddings, 2px the two 1px borders. */
.cta { --cta-h: calc(1.65em + 1.4rem + 2px); }
.badge { display: inline-flex; align-items: center; }
.badge img { height: var(--cta-h); width: auto; display: block; }
/* Not a link yet. Kept in place rather than hidden so the three read as a set,
   and dimmed so nobody clicks twice wondering why nothing happened. */
.badge.soon { opacity: 0.4; cursor: default; filter: grayscale(1); }
/* The rule between the buttons and the stores, at every width.
   `flex-basis: 100%` both draws the division and causes it: a full-width flex
   item takes a line of its own, so everything after it starts on the next one.
   The badges are therefore always below the buttons and always left-aligned
   with them, with no wrapping for the CSS to detect and no width at which the
   answer changes.

   **This replaced a pair.** There was an upright rule for when the two groups
   shared a line and this one for when they did not, chosen between by container
   queries at a measured threshold per language. It worked, and the thresholds
   had to be re-swept after any change to a label, a badge, a gap or the rule's
   own margins — three times in one afternoon — and each re-sweep had the same
   trap in it: measure while the queries are still deciding for themselves and
   the upright rule is hidden throughout, so the width you find is ~41px short
   and the query then shows the rule at a width it overflows, leaving the stray
   tick the whole mechanism existed to prevent. A layout with one state cannot
   have a wrong one.

   It fades out to the right rather than running the full width. A rule that
   reaches the far margin would be the widest thing in the hero and would read
   as a section break, dividing the page rather than the row; fading it lets it
   separate the two groups and then get out of the way. Gone by 62%, which is
   past the right edge of both the badges above it and the buttons below — it
   covers what it divides, and the fade means it stops being a line some way
   before that. */
.cta-rule {
  flex-basis: 100%;
  height: 1px;
  background: linear-gradient(to right, var(--line), transparent 62%);
}
