/*
 * wowui.app — one stylesheet, no build step.
 *
 * Four documents do not justify a second toolchain, so the app's design tokens are restated
 * here by hand. The source of truth is apps/desktop/src/styles.css; a token that changes
 * there changes here, and there is no third place.
 *
 * The tokens are the app's new ones. The old set was a navy: every surface and every text
 * step carried chroma 2.5 to 5.4 on one blue-violet hue, which is a sixth accent painted
 * across the whole screen. Measured in OKLCH and taken out; every step keeps its exact
 * lightness, so nothing about contrast moved.
 *
 * What is *not* copied is the composition. A desktop window sits in a frame the OS drew and
 * can stack equal panels down a column; a landing page that does the same reads as a
 * template with the colours swapped -- a label over every section, three cards with numbers
 * in circles, an accordion at the bottom. So:
 *
 *  - the product is the hero, and the largest thing on the page is a picture of it;
 *  - chapters alternate axis and measure instead of stacking at one width;
 *  - figures are set in tabular numerals and are measured, never adjectives;
 *  - the accent appears about three times in the whole document.
 */

/* ==========================================================================
   1. Typeface — self-hosted
   ==========================================================================
   The same face the app uses, from this origin. Not Google Fonts: that is a request to a
   third party on every page view, which the privacy page would have to list, and which
   Austrian and German courts have already ruled on. Two variable subsets, 133 KB together.
   ========================================================================== */

@font-face {
  font-family: "Inter Variable";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("font/inter-latin-wght-normal.woff2") format("woff2-variations");
  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+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Inter Variable";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("font/inter-latin-ext-wght-normal.woff2") format("woff2-variations");
  unicode-range:
    U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ==========================================================================
   2. Tokens
   ========================================================================== */

:root {
  --app-background: #111111;
  --app-surface: #1b1b1b;
  --app-card: #242424;
  --app-border: #363636;

  --text-primary: #ffffff;
  --text-secondary: #c0c0c0;
  --text-muted: #8a8a8a;

  /* Two accents, two weights each. An accent has two jobs and they need different colours:
     on a fill or a border it only has to be seen (3:1), as text it has to be read (4.5).
     Blue measures 4.18 on the card surface and purple 3.22 -- under the floor. The soft
     steps are the same hue, lifted until they clear it everywhere.
     The rule: text takes the soft step, everything else takes the saturated one. */
  --brand-blue: #4f7cff;
  --brand-purple: #7c4dff;
  --brand-blue-soft: #71a3ff;
  --brand-purple-soft: #a681ff;

  --status-success: #22c55e;
  --status-error: #ef4444;

  --motion-fast: 120ms;
  --motion-base: 220ms;
  --motion-slow: 600ms;
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* Below the app's own background, so a picture of the app has somewhere to sit. */
  --ground: #0a0a0a;

  /* Hairlines, not borders. 1px at 8% reads as an edge; a solid line reads as a box. */
  --line: rgba(255, 255, 255, 0.08);
  --line-soft: rgba(255, 255, 255, 0.05);
  --line-strong: rgba(255, 255, 255, 0.14);

  --fill-1: rgba(255, 255, 255, 0.03);
  --fill-2: rgba(255, 255, 255, 0.06);
  --fill-3: rgba(255, 255, 255, 0.1);

  /* The app's own values, not approximations of them. They had drifted -- component 14
     against the app's 16, panel 20 against 24 -- which is small enough that nobody notices
     one panel and large enough that a listing page beside the app's own reads as a different
     product. `apps/desktop/src/styles.css` is the source; these walk across by hand. */
  --radius-sm: 10px;
  --radius-component: 16px;
  --radius-panel: 24px;
  --radius-pill: 999px;

  --shadow-frame: 0 80px 160px -50px rgba(0, 0, 0, 0.9);

  --header-height: 64px;
  --measure: 1120px;
  --measure-wide: 1400px;
  --gutter: 24px;

  color-scheme: dark;
}

/* ==========================================================================
   3. Base
   ========================================================================== */

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

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-height) + 24px);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background-color: var(--ground);
  color: var(--text-secondary);
  font-family: "Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1,
h2,
h3 {
  margin: 0;
  color: var(--text-primary);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

h1 {
  /* The floor is for a 320px phone, not for the desktop the clamp was written on. At 2.9rem
     "interface," alone is 234px of a 272px line -- it fits, and only just, and any longer
     word in a future headline would not. */
  font-size: clamp(2.4rem, 7.4vw, 5.25rem);
  letter-spacing: -0.045em;
}

h2 {
  font-size: clamp(2rem, 4.4vw, 3.25rem);
}

h3 {
  font-size: 1.125rem;
  font-weight: 620;
  letter-spacing: -0.015em;
  line-height: 1.3;
}

p {
  margin: 0;
}

a {
  color: var(--text-primary);
  text-decoration-color: var(--line-strong);
  text-underline-offset: 3px;
  transition: color var(--motion-fast) var(--ease);
}

a:hover {
  color: #fff;
  text-decoration-color: currentColor;
}

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

/* Pictures do not drag out of the page, and do not answer a right click.
 *
 * Half of it here and half in site.js: this stops the drag, and the two listeners at the
 * end of that file suppress the context menu over any media -- the same rule, and
 * deliberately the same shape, as LoewentarnTV, so two sites of one author do not have two
 * answers to the same question.
 *
 * `user-select: none` is here rather than there because a right click that opens no menu
 * still leaves a selection highlight behind it otherwise.
 */
img {
  display: block;
  max-width: 100%;
  -webkit-user-drag: none;
  user-select: none;
}

code {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.9em;
  color: var(--text-primary);
}

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

/* ==========================================================================
   4. Layout
   ==========================================================================
   Two measures, not one. Text sits at 1120 where a line stays readable; a picture of the
   app goes to 1400, because what is being shown is what it looks like, and a screenshot
   boxed to the text column is a screenshot nobody can read.
   ========================================================================== */

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

.wrap--wide {
  max-width: var(--measure-wide);
}

.section {
  position: relative;
  padding-block: clamp(96px, 14vw, 180px);
}

/* A section that continues the one before it rather than starting a subject.
   Between two chapters the gap is one clamp: 168px at any usual width. Between two
   sections it is two lots of padding: 360px. So the safety chapter sat more than twice
   as far from the chapter it answers as that chapter sat from the one above it, and the
   rhythm broke at exactly the join that is meant to read as a continuation.

   Written as one gap between the pair rather than half a gap each, so the pair keeps the
   chapter rhythm and neither section has to know the other's number. Without :has() the
   first keeps its padding and the gap stays as it was -- wider than it should be, which
   is what it has been until now. */
.section:has(+ .section--continues) {
  padding-bottom: 0;
}

.section--continues {
  padding-top: clamp(96px, 13vw, 168px);
}

.rule {
  height: 1px;
  border: 0;
  margin: 0;
  background-color: var(--line-soft);
}

/* A join that has a line in it does not also need two full sections of padding around it.

   Between two sections the gap is two clamps -- up to 360px -- which is right where nothing
   marks the change and one thing has to end before the next begins. A rule marks it, so the
   space is doing the same job twice, and the line ends up alone in the middle of a screen
   of nothing. Same reasoning as .section--continues above, and the same :has(): the pair
   is spaced as a pair, and neither section has to know it is in one. */
.section:has(+ .rule) {
  padding-bottom: clamp(64px, 8vw, 104px);
}

.rule + .section {
  padding-top: clamp(64px, 8vw, 104px);
}

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

.nobr {
  white-space: nowrap;
}

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

/*
 * Reveal on scroll, and the reason it is here at all: a page this long, scrolled quickly,
 * reads as one flat wall of dark. Eight pixels and an opacity, once, per block -- enough
 * that the eye registers a new subject and not enough to be an effect. Set by the observer
 * at the bottom of the document; without JavaScript nothing is ever hidden.
 */
[data-reveal] {
  opacity: 1;
}

/* Never on an ancestor of something with backdrop-filter. opacity and transform both make
   an element a backdrop root, and a backdrop root is the end of what a descendant's blur can
   see -- so the glass works while this is animating and dies when it stops. The export
   chapter is left out of the reveal for that reason. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity var(--motion-slow) var(--ease),
    transform var(--motion-slow) var(--ease);
}

.js [data-reveal][data-shown="true"] {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .js [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   5. Atmosphere — the one place the game's own artwork shows
   ==========================================================================
   Twenty real interfaces, tiled. It replaced a painted one, and the painting had replaced an
   abstract wash before that -- each step for the same reason. The wash was a brand colour
   pretending to be a mood; the painting was closer but invented, and a headline reading
   "Your interface, in one file." over two interfaces nobody has ever played is the kind of
   thing a WoW player notices in a second. These are photographs of the thing being sold.

   Built by `build-wall.ps1` out of the author's own screenshots, which is the whole reason
   it can exist: scraping twenty strangers' UIs onto the page that advertises a paid tier
   would be the exact thing this product refuses to do everywhere else -- a package names
   AddOns rather than carrying them, because other people's work is not ours to redistribute.

   Three things are baked into the image rather than left to this stylesheet, because a CSS
   scrim strong enough to do any of them would have flattened the tiles as well: the dark
   pool the headline sits in, a ceiling so the navigation never lands on somebody's action
   bar, and a floor so the wall meets the page instead of being cut off by it.

   Still tamed here, and for the older reason: at full strength twenty screenshots are the
   only saturated thing on a page built out of neutral greys, and they would compete with the
   app screenshot directly below them for "the thing you are looking at".
   ========================================================================== */

.backdrop {
  position: absolute;
  inset: 0 0 auto;
  height: min(1180px, 132vh);
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

/* For a page shorter than the wall.
 *
 * The full height is 132vh, which is taller than any viewport by definition -- fine under a
 * landing page or a legal notice, both of which scroll well past it. `confirmed.html` is one
 * card on a `min-height: 70svh` grid, so an absolutely positioned 1180px block would have
 * hung below the footer and added empty scroll to a page whose entire message is one
 * sentence. Capped under 100vh instead, which is the property that matters: it can never
 * make a page taller than the screen it is read on. */
.backdrop--short {
  height: min(720px, 92vh);
}

.backdrop::before,
.backdrop::after {
  content: "";
  position: absolute;
  inset: 0;
}

.backdrop::before {
  background-image: url("img/background.jpg");
  /* `cover`, and not out of laziness. The dark pool this headline sits in belongs to the
     picture, so scaling the picture down scales the pool down with it -- tried at 92% with
     the painting that came before, and its two icon grids closed in until "Your interface,"
     was set on top of them. Make the band shorter if it ever needs to be smaller. */
  background-size: cover;
  background-position: 50% 30%;
  filter: saturate(0.7);
  opacity: 0.72;
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 0%,
    #000 42%,
    transparent 100%
  );
  mask-image: linear-gradient(to bottom, #000 0%, #000 42%, transparent 100%);
}

/* Two scrims, each for a different failure. The radial keeps the middle clear so the
   headline never sits on a lit tower; the linear brings the whole thing down to the ground
   colour before the first chapter, so nothing below the fold is read over a painting. */
.backdrop::after {
  background-image:
    radial-gradient(
      64% 56% at 50% 26%,
      transparent 0%,
      color-mix(in srgb, var(--ground) 54%, transparent) 100%
    ),
    linear-gradient(
      to bottom,
      color-mix(in srgb, var(--ground) 18%, transparent) 0%,
      color-mix(in srgb, var(--ground) 46%, transparent) 46%,
      var(--ground) 94%
    );
}

/* Anything the backdrop must not paint over. .page carries the landing page; main and the
   footer carry the documents, which have no wrapper of their own -- and without this the
   scrim sat on top of the text and greyed a whole legal notice.
   Written for `.atmosphere`, the faint gradient the documents used before the wall existed,
   and it is what let those pages move onto `.backdrop` without a single new rule: the thing
   underneath changed, the thing that must stay above it did not. */
.page,
main,
.site-footer {
  position: relative;
  z-index: 1;
}

/* ==========================================================================
   6. Header
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  height: var(--header-height);
  display: flex;
  align-items: center;
  border-bottom: 1px solid transparent;
  transition:
    border-color var(--motion-base) var(--ease),
    background-color var(--motion-base) var(--ease);
}

/* The bar earns its edge once there is something behind it. */
.site-header[data-scrolled="true"] {
  border-bottom-color: var(--line);
  background-color: color-mix(in srgb, var(--ground) 78%, transparent);
  /* The one element on this page glass belongs on: chrome, thin, almost no text, and
     something different behind it at every scroll position. -webkit- for Safari, which
     still wants the prefix. Saturation matters more than the blur radius -- a blur alone
     averages colour towards grey and reads as fog rather than as a pane. */
  -webkit-backdrop-filter: saturate(170%) blur(18px);
  backdrop-filter: saturate(170%) blur(18px);
}

/* Three children became four when Discord arrived, and `space-between` centred the nav
   only for as long as the brand and the button happened to weigh the same. The flanks now
   take an equal share and the nav takes its own width, so the menu sits in the middle of
   the page whatever is added beside it. */
.site-header .wrap {
  display: flex;
  align-items: center;
  gap: 24px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--text-primary);
  font-weight: 620;
  font-size: 1rem;
  letter-spacing: -0.02em;
  text-decoration: none;
}

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

.site-nav {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 32px;
  font-size: 0.9375rem;
}

/* The two flanks, equal by declaration rather than by coincidence. */
.site-header .brand,
.header-actions {
  flex: 1 1 0;
}

.header-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
}

.site-nav a {
  color: var(--text-muted);
  text-decoration: none;
}

.site-nav a:hover {
  color: var(--text-primary);
}

@media (max-width: 860px) {
  .site-nav {
    display: none;
  }
}

/* ==========================================================================
   7. Buttons
   ========================================================================== */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 13px 26px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition:
    transform var(--motion-fast) var(--ease),
    background-color var(--motion-base) var(--ease),
    border-color var(--motion-base) var(--ease);
}

.button:hover {
  transform: translateY(-1px);
}

.button:active {
  transform: scale(0.99);
}

/* The app's own primary, not a white pill.
 *
 * It was white with dark text, described here as a placeholder until there was a real call
 * to action. There is one now -- the download -- and it should look like the button that
 * does the equivalent job in the app: `bg-brand-blue text-white`, the same one "Install UI"
 * wears, dimmed to 85% on hover. A visitor who downloads here and opens the app a minute
 * later should meet the same blue.
 *
 * The hover rule is not decoration. `a:hover` in the base layer sets `color: #fff` and
 * scores (0,1,1), which beats `.button--primary` at (0,1,0) -- so a primary button that is
 * a link had white text on hover whatever this rule said. On the white pill that was white
 * on white: the label vanished under the pointer. Stated at the same weight as the thing it
 * has to beat rather than with !important, and the same trap already caught the CTA on the
 * listing page. */
.button--primary,
a.button--primary:hover {
  background-color: var(--brand-blue);
  border-color: var(--brand-blue);
  color: #fff;
}

a.button--primary:hover {
  background-color: color-mix(in srgb, var(--brand-blue) 85%, transparent);
  border-color: color-mix(in srgb, var(--brand-blue) 80%, transparent);
}

.button--ghost {
  border-color: var(--line-strong);
  color: var(--text-primary);
}

.button--ghost:hover {
  border-color: rgba(255, 255, 255, 0.3);
  background-color: var(--fill-1);
}

.button--small {
  padding: 8px 16px;
  font-size: 0.875rem;
}

/* Where the download will be, holding its place rather than filling it. It keeps the
   button's size so the fold does not move when the real one returns, and loses everything
   that says press me. A span in the markup for the same reason: this is a state, not a
   control, and a control that does nothing is worse than one that is not offered. */
.button--soon {
  border-color: var(--line);
  background-color: var(--fill-1);
  color: var(--text-muted);
  cursor: default;
}

.button--soon:hover,
.button--soon:active {
  transform: none;
}

/* ==========================================================================
   8. The fold
   ==========================================================================
   Headline, one line, and then the product at the largest size the viewport allows. The
   picture is the argument; the type is the caption on it.
   ========================================================================== */

.fold {
  position: relative;
  padding-top: clamp(72px, 12vw, 132px);
  text-align: center;
}

.fold h1 {
  margin-inline: auto;
  max-width: 16ch;
}

.fold-lede {
  margin: 26px auto 0;
  max-width: 46ch;
  color: var(--text-secondary);
  font-size: clamp(1.0625rem, 1.5vw, 1.25rem);
  line-height: 1.55;
  text-wrap: pretty;
}

.fold-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 34px;
}

/* Four facts, four objects.
   They were one grey sentence joined by middots, so the eye read a sentence and took none
   of them — and they are the four things somebody checks before deciding whether this is
   for them at all: cost, commitment, their OS, their game. Each gets its own outline, and
   the middots go: a separator that exists to hold a list apart is doing a border's job in
   punctuation.

   Deliberately not four ticks. A tick is an answer to a yes/no question, and "Windows 10
   and 11" is not one — it is a fact that excludes half the people reading it, and dressing
   an exclusion as a reassurance is the sort of small lie a landing page should not tell. */
/* The fine print of the button above it: what Windows will say, and which version this
 is. Muted and narrow, because it is a caption -- but in the fold, because the alternative
 was letting SmartScreen break the news. */
.fold-note {
  max-width: 54ch;
  margin: 18px auto 0;
  color: var(--text-muted);
  font-size: 0.875rem;
  text-wrap: pretty;
}

/* The separator hangs off the version rather than off the sentence after it. As an
   adjacent-sibling rule it printed a leading " · " whenever the version was not known yet --
   which is every render before the feed answers, and every render where it never does. */
.fold-note span[data-download-meta]:not(:empty)::after {
  content: " · ";
}

.fold-note em {
  color: var(--text-secondary);
  font-style: normal;
}

.fold-meta {
  display: flex;
  flex-wrap: wrap;
  /* The fold centres with text-align, which a flex container does not inherit for its own
     children: the row ran to the left edge under a heading, a paragraph and two buttons
     that are all centred. The actions row above has said justify-content since it was
     written; this one only became a flex row later, and did not. */
  justify-content: center;
  gap: 8px;
  margin-top: 22px;
}

.fold-meta span {
  padding: 6px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background-color: var(--fill-1);
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.3;
  white-space: nowrap;
}

/* The first is the one that decides whether anybody reads the other three. */
.fold-meta span:first-child {
  border-color: color-mix(in srgb, var(--status-success) 35%, transparent);
  color: var(--text-primary);
}

/* The window runs off the fold rather than floating in the middle of nowhere: the crop is
   what says there is more of it than fits.

   At the wide measure it is 875px tall under ~430px of heading, lede, buttons and chips,
   so on a 900px screen a tenth of it is visible and the crop stops reading as a crop --
   it reads as a picture that did not load. Narrower is not smaller here: less of it is cut
   off, so more of the app is actually on screen. It also stops competing with the text: a
   fold is a sentence with a picture under it, not a picture with a caption. */
.fold-shot {
  max-width: 1180px;
  margin-top: clamp(48px, 6vw, 76px);
  -webkit-mask-image: linear-gradient(to bottom, #000 72%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 72%, transparent 100%);
}

/* ==========================================================================
   9. The specimen — one real package
   ==========================================================================
   Numbers belong to something. Presented as facts about the product they were false the
   moment a second package existed; attributed to a package that exists they are precise,
   and they are the answer to the question the chapter above them raises: what does
   "one file" actually hold.
   ========================================================================== */

/* The specimen hangs off the screenshot, so it lives in the screenshot s column -- not
   across the chapter. Full width it rose into the copy on the left and put the file name
   through the line about the game running: an overlap needs something above it to overlap,
   and on the left there was nothing there.

   The shot fades out at the bottom and the card rides into the fade, which is the join the
   fold makes. Scoped with :has() so only a shot with a specimen under it loses its edge.
   Without :has() the card simply sits below the picture -- it stops overlapping, nothing
   breaks. */
.chapter-shot {
  display: grid;
  gap: clamp(14px, 2vw, 22px);
  min-width: 0;
}

/* A chapter may show more than one picture. They stack rather than sit side by side: half
   a column each would make them smaller than the thing they are meant to let you read. */
.chapter-shot .shot + .shot {
  margin-top: 0;
}

/* Only the picture the card actually meets. With two shots stacked in one column the
   rule reached both, and the upper one faded out into the middle of the chapter. */
/* Under a carousel only one shot is on screen, so the fade belongs to whichever that is;
   without one the pictures stack and only the bottom one meets the card. */
/* Fading late, because the card in front of it is glass and glass needs something behind it.
   It faded from 74% and the card was laid over the part that had already gone, so the pane
   had page ground behind it and nothing to frost. Sampled: the lower third of this
   screenshot averages L=0.023 with sparse white text in it, which through a 22px blur is a
   dark field with faint structure -- subtle, because the source is a dark interface, but
   visible, which flat ground never is. */
.chapter-shot[data-carousel]:has(.specimen) .shot,
.chapter-shot:has(.specimen) .shot:last-of-type {
  -webkit-mask-image: linear-gradient(to bottom, #000 93%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 93%, transparent 100%);
}

.specimen {
  /* Inset, so it lies *on* the picture rather than continuing it. Edge to edge it was the
     same width as the shot with a seam between them, which reads as one tall block that
     changed its mind halfway down -- not as a card resting on a screenshot. The shadow is
     what makes the inset legible: without it the card is a lighter rectangle rather than a
     nearer one. */
  position: relative;
  z-index: 1;
  /* The overlap is a trade, and this is the low end of it. Every pixel further down gives
     the screenshot back and takes backdrop away from the pane: at -210 the card was eating
     the picture it is a caption to, at -110 it covers under a quarter of it and still has
     interface behind its top half. Much less and the glass stops reading -- a pane needs
     roughly a third of its face over something before anybody can tell it is one. */
  /* The side inset was 5%, which left the card almost as wide as the screenshot it lies on
     -- near enough that the eye read them as one stack rather than as a card resting on a
     picture. At 11% it is clearly the smaller of the two, which is what a caption should be.
     The vertical overlap is unchanged: a shorter card at the same overlap has *more* of its
     face over the picture, which is what makes the glass read. */
  margin: clamp(-110px, -7.5vw, -64px) clamp(28px, 11%, 120px) 0;
  border-radius: var(--radius-panel);
  /* Frosted glass, and the mistake in every attempt before this one was the direction.
     A pane is *lighter* than what is behind it -- that is the whole visual signature, and
     it is why a light pane on a bright photo reads instantly as glass. Every pass here
     tinted it darker, with app-surface at 88, then 78, then 68 per cent, so the card was
     always a dark panel on a dark page however much blur went behind it.

     White at 8%, so the card sits above its surroundings rather than below them, with a
     diagonal falloff because real glass is not evenly lit. The backdrop is the lower third
     of the export screenshot, sampled at L=0.023 average. Thin: about 3% white, so
     thirty-two thirty-thirds of the backdrop comes through. It composites to roughly
     L=0.06-0.15, where white text reads 9.5:1 to 5.3:1. The softer lines at 78% white sit
     at 4.4:1 over the brightest patch a blurred dark interface produces -- the floor is
     4.5, and this is where the pane stops getting thinner. Any less fill and the caption
     goes under it. They are white rather than the grey they used to be
     for that reason: grey on a light pane is the one thing that cannot work.

     The saturation is what stops the blur reading as fog: averaging pulls colour towards
     grey, and pushing it back is what makes light look like it passed through something. */
  background-image: linear-gradient(
    135deg,
    rgb(255 255 255 / 5%) 0%,
    rgb(255 255 255 / 2.5%) 55%,
    rgb(255 255 255 / 3.5%) 100%
  );
  /* 18px rather than 22. A wide blur averages a dark interface into one flat tone, which is
     the fog this kept coming out as; a narrower one leaves the shapes of rows and panels
     recognisable, and recognising something through a pane is what makes it read as a pane. */
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  backdrop-filter: blur(18px) saturate(180%);
  border: 1px solid rgb(255 255 255 / 18%);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 22%),
    0 24px 50px -20px rgb(0 0 0 / 85%);
  overflow: hidden;
}

/* Nothing to frost without the filter, so the pane closes to the app surface instead of
   staying a white veil over the picture. */
@supports not (
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
) {
  .specimen {
    background-image: none;
    background-color: var(--app-surface);
  }
}

.specimen-file {
  display: flex;
  /* Centred, not on a shared baseline. Baseline is right when two things on a line are one
     line tall; the size is two -- a figure with a note under it -- so sharing a baseline
     pinned the file name to the *first* of them and left it sitting high in a row it is
     half of. Centring measures it against the whole stack opposite, which is what the row
     actually is. */
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 11px 16px;
  /* The dividers are white on a white pane, not the page's own line colour: on glass a
     dark hairline reads as a crack rather than as an edge. */
  border-bottom: 1px solid rgb(255 255 255 / 14%);
}

/* Weighted to match the size opposite it, which took two goes.
   The name was 14px regular against a 15px semibold figure, so the header read as one thing
   with a label beside it rather than as two facts about one file. Setting both to 15 did not
   fix it: a monospace face at a given size looks smaller than a proportional one at the same
   size -- its letters are set on a fixed advance, so the glyphs sit lighter in more space.
   Matching the numbers is not matching the impression, so this is a point larger than the
   figure it faces rather than equal to it.

   Left and right rather than centred *across* the row: they are a pair, and a pair reads as
   a pair when it sits at the two ends of the line it shares. Centred *within* it, though --
   see `align-items` above. */
.specimen-name {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  /* A file name is one long unbreakable token, and a monospace one is the widest form of
     it. Without these it pushed the size clean off the card on a phone rather than
     yielding. */
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--text-primary);
}

/* The size is a figure, not a footnote. It was 13px grey beside the file name while the
   screenshot under it said "132 MB before compression" in a tile -- two numbers about one
   package, one of them looking like a caption, and the reader left to work out that they
   measure different things. It says which it is now, and it is legible enough to be read
   before the tiles below it. */
.specimen-size {
  display: grid;
  justify-items: end;
  gap: 1px;
  color: var(--text-primary);
  font-size: 0.9375rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  text-align: right;
}

.specimen-size span {
  color: rgb(255 255 255 / 78%);
  font-size: 0.6875rem;
  font-weight: 400;
  letter-spacing: 0.02em;
}

/* Dividers drawn, not left as gaps.
   The gap trick works by letting the parent's colour show between opaque cells -- and the
   cells became transparent when this card became glass, so the parent showed through all
   of them instead of only between them. The grid turned into one lighter band with no
   columns in it, which is what "no structure any more" was. Borders belong to the cell, so
   they cannot leak. */
.specimen-facts {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  margin: 0;
}

.specimen-facts > div + div {
  border-left: 1px solid rgb(255 255 255 / 14%);
}

/* Once the chapter stacks, the card has the whole page but a phone s worth of it. Two
   columns means the divider belongs to every second cell, and the second row needs a line
   above it that a single-row layout never did. */
@media (max-width: 640px) {
  /* The header is a two-column row of a 25-character file name and a size with a note under
     it. Measured at 375px: the card has 258px inside it and that row wants about 416. It
     stacks here, and the size stops being right-aligned -- ranged right under a name that
     starts at the left edge is two alignments in four inches. */
  .specimen-file {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }

  .specimen-size {
    justify-items: start;
    text-align: left;
  }

  .specimen-name {
    font-size: 0.875rem;
  }

  /* Less of a bite out of a picture that is already only as tall as the phone allows. */
  .specimen {
    margin-top: -48px;
  }

  .specimen-facts {
    grid-template-columns: repeat(2, 1fr);
  }

  .specimen-facts > div + div {
    border-left: 0;
  }

  .specimen-facts > div:nth-child(even) {
    border-left: 1px solid var(--line);
  }

  .specimen-facts > div:nth-child(n + 3) {
    border-top: 1px solid var(--line);
  }
}

/* Four across, and the type sized to fit four rather than the band sized to fit the type.
   Two rows fitted, and the card became the tallest thing in the chapter -- a footnote about
   one file standing taller than the screenshot it belongs to. This is a caption with
   numbers in it, so it is scaled like one. */
.specimen-facts > div {
  /* Centred in the cell. Ranged left, four figures of different widths left four ragged
     starts under one straight row of dividers, which is the arrangement that makes a table
     look untidy however carefully it is spaced. */
  padding: 11px 12px;
  text-align: center;
}

.specimen-facts dt {
  color: var(--text-primary);
  font-size: clamp(1rem, 1.15vw, 1.1875rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}

.specimen-facts dd {
  margin: 3px 0 0;
  color: rgb(255 255 255 / 78%);
  font-size: 0.75rem;
  line-height: 1.35;
  text-wrap: balance;
}

.specimen figcaption {
  /* Centred, with the figures above it. Ranged left under four centred columns it was the
     one line on the card that started somewhere else. */
  padding: 10px 16px;
  text-align: center;
  border-top: 1px solid rgb(255 255 255 / 14%);
  color: rgb(255 255 255 / 78%);
  font-size: 0.75rem;
  text-wrap: pretty;
}

/* ==========================================================================
   10. Chapters — one per thing the app does
   ==========================================================================
   Alternating axis rather than a stack of equal cards. Each chapter is named after the
   button it describes, says what that button does, and shows the one picture that proves
   it. Which side the picture sits on is a property of the chapter's position, not of the
   chapter: every insertion above swings everything below.
   ========================================================================== */

.chapter {
  display: grid;
  gap: clamp(40px, 6vw, 88px);
  align-items: center;
  grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr);
}

.chapter + .chapter {
  margin-top: clamp(96px, 13vw, 168px);
}

/* The flip swaps the order, so it has to swap the widths with it.
   The columns are 0.78fr of prose and 1.22fr of picture, and `order` moves the elements
   without moving the tracks -- so in a flipped chapter the screenshot landed in the narrow
   column and the paragraph in the wide one. The import chapter's picture came out a third
   smaller than the two beside it, which is exactly the wrong picture to shrink: it is the
   one whose whole argument is that you can read what the import will do. */
.chapter--flip {
  grid-template-columns: minmax(0, 1.22fr) minmax(0, 0.78fr);
}

.chapter--flip .chapter-copy {
  order: 2;
}

@media (max-width: 940px) {
  .chapter {
    grid-template-columns: 1fr;
  }

  .chapter--flip .chapter-copy {
    order: 0;
  }
}

.chapter-copy h2 {
  font-size: clamp(1.75rem, 3.2vw, 2.5rem);
}

.chapter-copy p {
  margin-top: 20px;
  max-width: 42ch;
  color: var(--text-secondary);
  font-size: 1.0625rem;
  text-wrap: pretty;
}

.chapter-copy p + p {
  margin-top: 14px;
}

.chapter-note {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: 26px;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.chapter-note::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background-color: var(--status-success);
}

/* ==========================================================================
   11. Screenshot frames and placeholders
   ==========================================================================
   The frame is the same object whether it holds a picture or waits for one, so the layout
   is finished before the pictures are and nothing shifts when they land.
   ========================================================================== */

.shot {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  background-color: var(--app-background);
  box-shadow: var(--shadow-frame);
  overflow: hidden;
}

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

/* The rule above outranks the browser default for [hidden], so the carousel would hide
   nothing at all: every slide would stay on screen and the dots would do nothing. */
img.shot[hidden] {
  display: none;
}

.shot--placeholder {
  display: grid;
  place-items: center;
  aspect-ratio: 1584 / 991;
  background-color: var(--app-surface);
  background-image: repeating-linear-gradient(
    135deg,
    var(--fill-1) 0 14px,
    transparent 14px 28px
  );
}

.shot-label {
  display: grid;
  gap: 5px;
  justify-items: center;
  padding: 16px 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius-component);
  background-color: color-mix(in srgb, var(--app-background) 80%, transparent);
  text-align: center;
}

.shot-label strong {
  color: var(--text-primary);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.shot-label span {
  color: var(--text-muted);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

/* ==========================================================================
   12. The file — what is in it, and what is not
   ==========================================================================
   A ledger rather than cards: two columns of hairlines, the second deliberately the more
   interesting one. What a format leaves out is the decision it is made of.
   ========================================================================== */

.ledger {
  display: grid;
  gap: clamp(32px, 5vw, 72px);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (max-width: 860px) {
  .ledger {
    grid-template-columns: 1fr;
  }
}

.ledger h3 {
  padding-bottom: 18px;
  border-bottom: 1px solid var(--line);
  color: var(--text-muted);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.ledger li {
  display: grid;
  /* A column for the mark, and it spans both rows so the tick sits against the heading
     rather than floating between the heading and its sentence. */
  grid-template-columns: 20px minmax(0, 1fr);
  column-gap: 13px;
  row-gap: 4px;
  padding-block: 18px;
  border-bottom: 1px solid var(--line-soft);
}

/* A mask rather than eight inline SVGs: the shape is the same twice and only the colour
   differs, so the colour is the thing that should be written down twice. Both measure well
   past the 3:1 a symbol needs against this ground -- 8.69 and 5.26. */
.ledger li::before {
  content: "";
  grid-row: 1 / span 2;
  width: 19px;
  height: 19px;
  margin-top: 1px;
  background-color: var(--text-muted);
  -webkit-mask: var(--ledger-mark) center / contain no-repeat;
  mask: var(--ledger-mark) center / contain no-repeat;
}

[data-tone="yes"] li::before {
  --ledger-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m8.5 12.2 2.4 2.4 4.6-4.8'/%3E%3C/svg%3E");

  background-color: var(--status-success);
}

/* The mark, not the alarm. Red is this design's error colour, and every line in this
   column is a decision somebody made on purpose -- not shipping other people's code, not
   writing outside the folder you chose, not carrying other people's characters. Those are
   the strongest things on this page, and they were drawn in the colour the app uses when
   something has gone wrong: the heading said "deliberately" while the colour said
   "unfortunately".

   Muted, so the column reads as "not included" rather than as a list of shortcomings -- and
   so red still means something where it means an error. */
[data-tone="no"] li::before {
  --ledger-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m15 9-6 6M9 9l6 6'/%3E%3C/svg%3E");

  background-color: var(--text-muted);
}

.ledger strong {
  color: var(--text-primary);
  font-size: 1rem;
  font-weight: 600;
}

.ledger p {
  color: var(--text-muted);
  font-size: 0.875rem;
  text-wrap: pretty;
}

/* ==========================================================================
   13. Footer
   ========================================================================== */

.site-footer {
  padding-block: 72px 40px;
  border-top: 1px solid var(--line);
}

.footer-grid {
  display: grid;
  gap: 48px;
  grid-template-columns: minmax(0, 1.7fr) repeat(3, minmax(0, 1fr));
}

@media (max-width: 860px) {
  .footer-grid {
    grid-template-columns: 1fr 1fr;
  }
}

.footer-about p {
  margin-top: 16px;
  max-width: 30ch;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.footer-grid h3 {
  margin-bottom: 16px;
  color: var(--text-muted);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.footer-grid li + li {
  margin-top: 10px;
}

.footer-grid a {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  text-decoration: none;
}

.footer-grid a:hover {
  color: var(--text-primary);
}

/* A link that leaves the page says so with a glyph rather than a box: it marks the two
   contact links as actions without giving one column a shape the others do not have. */
.footer-out::after {
  content: "↗";
  margin-left: 6px;
  color: var(--text-muted);
  font-size: 0.8125rem;
}

.footer-grid a:hover .footer-out::after,
.footer-out:hover::after {
  color: inherit;
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 56px;
  padding-top: 24px;
  border-top: 1px solid var(--line-soft);
  color: var(--text-muted);
  font-size: 0.8125rem;
}

/* ==========================================================================
   14. The other four documents
   ==========================================================================
   legal, privacy, terms and confirmed share this stylesheet and a smaller chrome. Kept
   here rather than duplicated per page for the reason the whole file exists: there is no
   build step, so a second copy is a second thing to forget.
   ========================================================================== */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 20px;
  border-radius: 0 0 var(--radius-sm) 0;
  background-color: var(--app-card);
  color: var(--text-primary);
  text-decoration: none;
}

.skip-link:focus-visible {
  left: 0;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.doc {
  max-width: 44rem;
  margin-inline: auto;
  padding-block: clamp(56px, 8vw, 96px);
}

.doc h1 {
  font-size: clamp(2rem, 4vw, 2.75rem);
  margin-bottom: 12px;
}

.doc h2 {
  margin-top: 44px;
  margin-bottom: 12px;
  font-size: 1.25rem;
}

.doc h3 {
  margin-top: 28px;
  margin-bottom: 8px;
}

.doc p,
.doc li {
  color: var(--text-secondary);
  font-size: 0.9375rem;
}

.doc p + p,
.doc ul {
  margin-top: 12px;
}

.doc ul {
  list-style: disc;
  padding-left: 22px;
}

.doc li + li {
  margin-top: 6px;
}

/* Every link in a document except the ones that are buttons.
 *
 * Without the exclusion this paints them too, and it wins: `.doc a` scores (0,1,1) against
 * `.button--primary` at (0,1,0), so the label came out brand-blue-soft on a brand-blue
 * ground -- blue on blue, barely legible -- and turned white only on hover, because the
 * hover rule is an element plus two classes and does beat it. A ghost button in a document
 * had the same problem more quietly.
 *
 * Fixed here rather than by making every button rule heavier: a button is not body text with
 * a link colour, and the rule that should stop is this one. */
.doc a:not(.button) {
  color: var(--brand-blue-soft);
}

/* ---------- the legal notice, in two languages ---------- */

.doc-langs {
  margin-bottom: 28px;
}

/* Sized to sit level with the ghost button beside it in the header: 3 + (5 + 22.4 + 5) + 3
   plus the hairline is 40.4px, which is what .button--small measures. A switch that is two
   pixels taller than the button next to it is the kind of thing nobody names and everybody
   sees. */
.lang-tabs {
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background-color: var(--fill-1);
}

.lang-tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 14px;
  border: 0;
  border-radius: var(--radius-pill);
  background-color: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 560;
  cursor: pointer;
  transition:
    background-color var(--motion-base) var(--ease),
    color var(--motion-base) var(--ease);
}

.lang-tab:hover {
  color: var(--text-secondary);
}

/* Selected reads off aria-selected rather than a second class: the attribute is what a
   screen reader is told, so letting it drive the paint keeps the two from disagreeing. */
.lang-tab[aria-selected="true"] {
  background-color: var(--fill-3);
  color: var(--text-primary);
}

/* An inline <svg> with a viewBox and no width or height has no intrinsic size, and inside a
   flex row it collapses to nothing -- which is why the flags were missing rather than wrong.
   2:1, the shape of the viewBox, so neither flag is stretched. The hairline is what keeps the
   black stripe of one and the dark blue of the other from dissolving into the dark bar. */
.lang-flag {
  flex: none;
  width: 20px;
  height: 10px;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgb(255 255 255 / 18%);
}

.doc-part[hidden] {
  display: none;
}

/* ---------- the address-confirmed page ---------- */

.result {
  display: grid;
  place-items: center;
  min-height: 70vh;
  min-height: 70svh;
  padding-block: 64px;
}

.result-card {
  max-width: 34rem;
  padding: 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  background-color: var(--fill-1);
  text-align: center;
}

.result-card h1 {
  font-size: clamp(1.75rem, 4vw, 2.25rem);
  margin-bottom: 14px;
}

.result-card p {
  color: var(--text-secondary);
}

.result-card p + p {
  margin-top: 14px;
}

.result-card .button {
  margin-top: 26px;
}

/* The line under the button needs its own rule: `p + p` cannot reach it, because what sits
   before it is the anchor. Without this the fine print touched the pill. */
.result-card .button + p {
  margin-top: 22px;
}

.result-hint {
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* ==========================================================================
   15. The lightbox — a screenshot at the size it was taken
   ==========================================================================
   A picture of an application is unreadable at a third of a page. The chapters show it in
   place; this shows it whole, and the arrows walk the chapter's own set.
   ========================================================================== */

img.shot[data-zoom="true"] {
  /* No focus ring of its own: :focus-visible is already global, and a second copy of the
     same two lines is a second place to change it. */
  cursor: zoom-in;
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(4px, 2vw, 24px);
  padding: clamp(16px, 4vw, 48px);
  /* Nearly opaque rather than a tint: the page behind it is a dark page, and a translucent
     scrim over dark grey leaves the screenshot competing with a blurred copy of itself. */
  background-color: rgb(6 6 6 / 94%);
}

.lightbox[hidden] {
  display: none;
}

/* The page does not scroll behind it. Without this the wheel moves the document while the
   picture stays put, and closing lands the reader somewhere they never went. */
.lightbox-open,
.lightbox-open body {
  overflow: hidden;
}

.lightbox-figure {
  display: grid;
  gap: 14px;
  justify-items: center;
  margin: 0;
  min-width: 0;
}

.lightbox-image {
  display: block;
  max-width: 100%;
  /* Room for the caption under it, so a wide picture is never cut off at the bottom edge
     of the viewport to make space for its own label. */
  /* svh, with vh underneath for browsers without it: on a phone vh is the height with the
     browser bars retracted, so a picture sized to it is taller than the screen it is being
     looked at on -- and this one is centred, so the overflow is split top and bottom where
     neither end can be scrolled to. */
  max-height: calc(100vh - clamp(110px, 18vh, 190px));
  max-height: calc(100svh - clamp(110px, 18svh, 190px));
  width: auto;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-frame);
}

.lightbox-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 12px;
  max-width: 70ch;
  color: var(--text-secondary);
  font-size: 0.875rem;
  text-align: center;
  text-wrap: pretty;
}

.lightbox-count {
  color: var(--text-muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.lightbox-step,
.lightbox-close {
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 999px;
  background-color: var(--fill-1);
  color: var(--text-primary);
  cursor: pointer;
  transition:
    background-color 140ms ease,
    border-color 140ms ease;
}

.lightbox-step:hover,
.lightbox-close:hover {
  border-color: var(--line-strong);
  background-color: var(--fill-2);
}

.lightbox-step {
  width: clamp(40px, 6vw, 56px);
  height: clamp(40px, 6vw, 56px);
  font-size: 1.75rem;
  line-height: 1;
}

.lightbox-step[hidden] {
  /* Hidden, not removed: the picture stays centred whether or not there is a set to walk. */
  visibility: hidden;
  display: grid;
}

.lightbox-close {
  position: absolute;
  top: clamp(12px, 3vw, 28px);
  right: clamp(12px, 3vw, 28px);
  width: 40px;
  height: 40px;
  font-size: 1.5rem;
  line-height: 1;
}

@media (max-width: 640px) {
  .lightbox {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Under the picture on a phone, where a 56px column either side is a third of the screen. */
  .lightbox-step {
    position: absolute;
    bottom: clamp(12px, 4vw, 24px);
  }

  .lightbox-step--prev {
    left: clamp(12px, 6vw, 32px);
  }

  .lightbox-step--next {
    right: clamp(12px, 6vw, 32px);
  }
}

/* ==========================================================================
   16. The carousel — one picture at a time, where a chapter has several
   ==========================================================================
   Stacked, two screenshots made the chapter twice as tall and each picture half as large,
   which defeats the only reason a picture of an application is here: that you can read it.
   ========================================================================== */

.carousel-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 4px;
}

.carousel-dots {
  display: flex;
  align-items: center;
  gap: 8px;
}

.carousel-step {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background-color: var(--fill-1);
  color: var(--text-secondary);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  transition:
    border-color var(--motion-fast) var(--ease),
    color var(--motion-fast) var(--ease);
}

.carousel-step:hover {
  border-color: var(--line-strong);
  color: var(--text-primary);
}

/* A named step, where the pictures are a sequence.
   Wrapping rather than scrolling sideways: seven labels do not fit on one line under about
   1200px, and a row that scrolls hides the steps a reader has not reached yet -- which is
   exactly the half that tells them how far this goes. */
.carousel-tab {
  padding: 7px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background-color: transparent;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition:
    border-color var(--motion-fast) var(--ease),
    color var(--motion-fast) var(--ease),
    background-color var(--motion-fast) var(--ease);
}

.carousel-tab:hover {
  border-color: var(--line-strong);
  color: var(--text-secondary);
}

/* The accent as an outline and a tint, not as a fill.
   White on brand-blue measures 3.71:1, which is fine for a mark somebody sees and not for
   12px text they have to read -- and these labels are the smallest type on the page. The
   soft step of the same accent on this ground is 7.91:1, and a filled pill among six
   outlined ones is a louder "you are here" than the page needs anyway. */
.carousel-tab[aria-current="true"] {
  border-color: color-mix(in srgb, var(--brand-blue) 55%, transparent);
  background-color: color-mix(in srgb, var(--brand-blue) 14%, transparent);
  color: var(--brand-blue-soft);
}

.carousel-caption {
  margin: 16px auto 0;
  max-width: 68ch;
  color: var(--text-muted);
  font-size: 0.875rem;
  text-align: center;
  text-wrap: pretty;
}

/* Capped against the viewport, not against the column.
   A 1584x991 screenshot at 1400px wide is 875px tall, so on a 768px laptop the picture
   alone is taller than the window: the step tabs and the caption are below the fold, and
   somebody on a small screen never learns the carousel is a sequence at all. They cannot
   use the one part of this page that needs using.

   width:auto rather than a max-height on a full-width image: with width:100% a height cap
   squashes the picture instead of scaling it, and object-fit would letterbox it into empty
   side bars. This way it is as large as fits both constraints, and centred.

   svh rather than vh: on a phone vh is the height with the browser bars retracted, which
   is not the height anybody is looking at when the page loads. */
.walkthrough .shot {
  width: auto;
  max-width: 100%;
  max-height: 62vh;
  max-height: 62svh;
  margin-inline: auto;
}

/* Stacked below 940px a chapter's picture has the whole column, and the same arithmetic
   applies to it. */
@media (max-width: 940px) {
  .chapter .shot {
    width: auto;
    max-width: 100%;
    max-height: 62vh;
    max-height: 62svh;
    margin-inline: auto;
  }
}

.walkthrough .carousel-dots {
  flex-wrap: wrap;
  justify-content: center;
}

.walkthrough .carousel-controls {
  margin-top: 18px;
}

/* A header centred over what it introduces.
   Left-aligned in the wide measure it sat against one edge of a picture that spans the
   whole of it, so the section read as two things that had been put near each other. And a
   heading takes its size from what it sits beside: the same clamp that is right over a
   1120px column is loud over a 1400px one, so this one steps down.

   The measure is capped as well. Over the wide wrap the lede ran to a line length nobody
   reads twice. */
.section-head {
  max-width: 46rem;
  margin-inline: auto;
  text-align: center;
}

.section-head h2 {
  font-size: clamp(1.75rem, 3.2vw, 2.5rem);
}

.section-lede {
  margin-top: 14px;
  max-width: 62ch;
  margin-inline: auto;
  color: var(--text-secondary);
  font-size: 1.0625rem;
  text-wrap: pretty;
}

.carousel-dot {
  width: 8px;
  height: 8px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background-color: var(--fill-3);
  cursor: pointer;
  transition:
    background-color var(--motion-fast) var(--ease),
    transform var(--motion-fast) var(--ease);
}

.carousel-dot[aria-current="true"] {
  background-color: var(--brand-blue);
  transform: scale(1.35);
}

/* The dot is 8px, which is smaller than anything should be to press. The target around it
   is not: the padding is transparent and the mark stays the size it is drawn. */
.carousel-dot::before {
  content: "";
  display: block;
  margin: -11px;
  padding: 11px;
}

/* ==========================================================================
   17. The beta form
   ==========================================================================
   The one page on this site that asks for something rather than saying something. It sits
   on the `.doc` shell the legal pages use -- same measure, same rhythm -- because it is read
   the same way: top to bottom, once.

   Every control is native. A styled radio is a div pretending to be a radio, and the
   pretence breaks on a keyboard, in a screen reader and in forced-colours mode; `accent-color`
   gets the brand into the real one with a single declaration and nothing to maintain.
   ========================================================================== */

.form-lede {
  /* Its own margin: the two notice boxes sit between this and the paragraph above it in
     the markup, so the stylesheet's adjacent-paragraph rule never matches even though
     nothing is drawn between them. */
  margin-top: 18px;
  color: var(--text-muted);
}

.beta-form {
  margin-top: 28px;
  accent-color: var(--brand-blue);
}

.beta-form fieldset {
  margin: 0 0 26px;
  padding: 22px 22px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  background-color: var(--fill-1);
}

.beta-form legend {
  padding: 0 10px;
  color: var(--text-primary);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.field {
  display: block;
  margin-bottom: 18px;
}

.field-label {
  display: block;
  margin-bottom: 8px;
  color: var(--text-primary);
  font-size: 0.9375rem;
  line-height: 1.45;
}

.field-label em {
  color: var(--text-muted);
  font-style: normal;
}

.field-input {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--app-border);
  border-radius: var(--radius-sm);
  background-color: rgb(0 0 0 / 25%);
  color: var(--text-primary);
  font: inherit;
  font-size: 0.9375rem;
}

.field-input:focus-visible {
  border-color: var(--brand-blue);
  outline: none;
  box-shadow: 0 0 0 3px rgb(79 124 255 / 20%);
}

.field-input--sub {
  margin-top: 8px;
}

.field-area {
  resize: vertical;
  line-height: 1.55;
}

/* Wrapping rather than a column: five short options down a page is five lines of mostly
   empty space, and the eye has to travel the whole height to see the set it is choosing
   from. */
.choices {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
}

.choices--stacked {
  flex-direction: column;
  gap: 14px;
}

.choices label {
  display: flex;
  align-items: baseline;
  gap: 8px;
  color: var(--text-secondary);
  font-size: 0.9375rem;
  line-height: 1.5;
  cursor: pointer;
}

.choices--stacked label {
  align-items: flex-start;
}

/* The label text belongs in one box. A flex label makes a flex item out of every inline
   element inside it, so a <code> or a link in a sentence was lifted out of the sentence and
   set beside it -- "Sicherung meines WTF - Ordners." broken across the label. The markup
   wraps the text in a span; this keeps it able to shrink so a long line still wraps. */
.choices label > span {
  min-width: 0;
}

.choices input {
  flex: none;
  /* Nudged down so a box lines up with the first line of a label that runs to three, rather
     than with the cap height of the first word. */
  margin-top: 0.35em;
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  margin-top: 4px;
}

/* The same row of buttons sits in two different neighbourhoods, and only one of them was
 * right. In the form it is a div after a fieldset, which already carries 26px below itself --
 * hence the 4px above. On the thank-you and error pages it is a paragraph after a paragraph,
 * so `.doc p + p` (0,1,2) beats `.form-actions` (0,1,0) and hands the buttons 12px: the gap
 * between two sentences, given to a control. They read as a fourth line of the paragraph.
 *
 * Specificity is the whole fix -- (0,2,1) is what it takes to outrank the neighbour rule --
 * and it is scoped to `.doc` so the form keeps the spacing that was measured for it. */
.doc p.form-actions {
  margin-top: 30px;
}

.form-fineprint {
  color: var(--text-muted);
  font-size: 0.8125rem;
}

/* The answer after the handler redirects. Hidden in the markup and shown by the page's own
   script, so a reader with scripting off never meets an empty box claiming something. */
.form-note {
  margin: 24px 0;
  padding: 16px 18px;
  border: 1px solid rgb(34 197 94 / 35%);
  border-radius: var(--radius-component);
  background-color: rgb(34 197 94 / 8%);
}

.form-note--bad {
  border-color: rgb(239 68 68 / 35%);
  background-color: rgb(239 68 68 / 8%);
}

/* Off the page for people, still reachable for the machines that fill it in. `display: none`
   is the one thing that does not work here: a bot reads the stylesheet too. */
.beta-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The answers handed back when the mail could not be sent. A <pre> rather than a rebuilt
   form: it is meant to be selected and pasted into a mail in one gesture, and every attempt
   to be cleverer than that ends in somebody losing the paragraph they just wrote. */
/* Gone when there is nothing in it.
 *
 * The markup holds <!--ANSWERS-->, which PHP swaps for the application. Opened directly --
 * or if that substitution ever failed -- the comment stays and a <pre> with no content still
 * draws its full frame: an empty box under a sentence promising the answers are below.
 *
 * `:empty` is the right test because CSS ignores comments when deciding it, so the box is
 * hidden exactly while the placeholder is still a placeholder and appears the moment the
 * handler has put something there. */
.answer-dump:empty {
  display: none;
}

.answer-dump {
  overflow-x: auto;
  margin: 20px 0;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-component);
  background-color: var(--fill-1);
  color: var(--text-secondary);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  line-height: 1.5;
  white-space: pre-wrap;
  /* The one place on the site where selecting text is the entire point. */
  user-select: text;
}

/* ---------------------------------------------------------------------------
   The Discord link in the header.
   --------------------------------------------------------------------------- */

/* Discord in the header, because that is where the people already using this are.

   A plain link. It carried live member and online counts for an afternoon, fetched from
   Discord and drawn beside a pulsing dot -- and a count of three is a true number that
   argues against joining, so it was hidden below a threshold. A widget that shows nothing
   until the number flatters is a request on every page load for a word the markup already
   has, so it went entirely rather than waiting for the room to fill. */
.discord-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  color: var(--text-secondary);
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration: none;
  transition:
    border-color var(--motion-fast) var(--ease),
    color var(--motion-fast) var(--ease);
}

.discord-pill:hover {
  border-color: var(--line-strong);
  color: #fff;
}

.discord-pill-mark {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: currentColor;
}


