/* Shaman download site.
   The material is the product's own: the honeycomb ground from the Projet
   tab, the neon law from glow.css, and the three finance colours. Nothing
   here is invented for a landing page.

   THE GLOW LAW, kept: light means live. Glow belongs to what is alive and
   actionable right now — the download button, the current version, the
   pulse travelling the comb. A page that glows everywhere says nothing.

   MOTION, and what it is for. Three registers, and each one answers a
   question the visitor is actually asking:
     · the ground moves because the app is a living thing, not a document;
     · the type assembles because the first sentence is the whole pitch and
       it should arrive as a sentence, not as a wall;
     · the screenshots turn to face you as they arrive, because they are
       objects and this page is the shop window.
   Everything here is transform and opacity — nothing animates a property
   that costs layout — and every last bit of it stands down under
   prefers-reduced-motion, at the bottom of this file. */

@font-face {
  font-family: "Aptos";
  src: url("/assets/Aptos.ttf") format("truetype");
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: "Aptos";
  src: url("/assets/Aptos-Bold.ttf") format("truetype");
  font-weight: 700; font-display: swap;
}
/* The DISPLAY cut, for the one sentence set large. Aptos Display is drawn
   for size — tighter fit, finer joins, less spacing between the letters —
   and at 70px the text cut the headline used to wear reads as an interface
   label blown up rather than as a headline. */
@font-face {
  font-family: "Aptos Display";
  src: url("/assets/Aptos-Display.ttf") format("truetype");
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: "Aptos Display";
  src: url("/assets/Aptos-Display-Bold.ttf") format("truetype");
  font-weight: 700; font-display: swap;
}
/* The MONO cut, for the name and for every number meant to be compared
   with another number. On the name it is load-bearing rather than a taste:
   the letters cycle, and in a proportional face every swap changes the
   word's width, so the whole thing shivers while it settles. In mono
   nothing moves but the glyphs. */
@font-face {
  font-family: "Aptos Mono";
  src: url("/assets/Aptos-Mono.ttf") format("truetype");
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: "Aptos Mono";
  src: url("/assets/Aptos-Mono-Bold.ttf") format("truetype");
  font-weight: 700; font-display: swap;
}

/* Cross-document view transitions: moving between the landing page and a
   version page is one continuous surface, not a reload. Ignored entirely
   where it is not supported, which is the whole appeal. */
@view-transition { navigation: auto; }

:root {
  --ground: #05050f;
  --panel: rgba(22, 22, 44, 0.55);
  --panel-2: rgba(30, 30, 58, 0.72);
  --line: rgba(255, 255, 255, 0.10);
  --line-2: rgba(255, 255, 255, 0.18);
  --ink: #f4f3ff;
  --dim: #9494b4;
  --dim-2: #b9b9d4;

  --gold: #FFD700;
  --gold-rgb: 255, 215, 0;
  --teal: #4ECDC4;
  --teal-rgb: 78, 205, 196;
  --pink: #E74C8C;
  --pink-rgb: 231, 76, 140;
  --blue: #3498DB;
  --blue-rgb: 52, 152, 219;

  --tone: var(--gold);
  --tone-rgb: var(--gold-rgb);
  --measure: 34rem;

  --font-display: "Aptos Display", "Aptos", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "Aptos Mono", ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;

  /* Easing. Nothing on this page uses `ease` — the browser default is the
     one curve that reads as "a computer did this". */
  --e-out: cubic-bezier(0.16, 1, 0.30, 1);
  --e-soft: cubic-bezier(0.22, 0.61, 0.36, 1);
  --e-back: cubic-bezier(0.34, 1.36, 0.64, 1);
}

* { box-sizing: border-box; }

img { height: auto; }

html { scroll-behavior: auto; overflow-x: clip; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: "Aptos", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 17px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

a { color: var(--gold); text-decoration: none; }
a:hover { text-decoration: underline; }
:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: 4px; }
::selection { background: rgba(var(--gold-rgb), 0.28); color: #fff; }

.wrap { max-width: 1180px; margin: 0 auto; padding: 0 30px; }

/* ═══ THE GROUND ═══════════════════════════════════════════════════
   Four fixed layers under everything. The canvas is the living comb;
   site.js owns it. The rest is atmosphere: a bloom that breathes on a
   period nothing else shares, a vignette that keeps the eye in the
   middle, and grain, which is the cheapest way to stop a dark gradient
   banding on a cheap panel — and reads as film while it does it. */

.ground, .ground-still, .bloom, .grain, .vignette {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 0;
}

.ground { width: 100%; height: 100%; }

/* The no-script lattice: the same geometry, stroked flat, with the two
   accent traces drawn once. `html.js` takes it away, because from that
   point the canvas above is painting the same comb and putting light
   through it — two lattices half a pixel apart is a moiré, not a texture. */
.ground-still { width: 100%; height: 100%; }
.ground-still .cells { stroke: #ffffff; stroke-width: 1; fill: none; opacity: 0.05; }
.ground-still .trace {
  fill: none;
  stroke: rgba(var(--gold-rgb), 0.28);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 6px rgba(var(--gold-rgb), 0.20));
}
.js .ground-still { display: none; }

.bloom {
  background:
    radial-gradient(58% 46% at 22% 8%, rgba(var(--gold-rgb), 0.14), transparent 68%),
    radial-gradient(52% 44% at 88% 26%, rgba(var(--teal-rgb), 0.09), transparent 70%),
    radial-gradient(64% 50% at 50% 104%, rgba(var(--blue-rgb), 0.10), transparent 72%);
  animation: breathe 23s var(--e-soft) infinite alternate;
}

@keyframes breathe {
  from { opacity: 0.72; transform: scale(1) translate3d(0, 0, 0); }
  to   { opacity: 1;    transform: scale(1.10) translate3d(0, -1.5%, 0); }
}

.vignette {
  background:
    radial-gradient(120% 88% at 50% 42%, transparent 42%, rgba(3, 3, 10, 0.62) 100%);
}

.grain {
  opacity: 0.035;
  background-repeat: repeat;
  background-size: 200px 200px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
}

main, footer { position: relative; z-index: 2; }

/* ═══ REVEAL PRIMITIVES ════════════════════════════════════════════
   Under no-JS none of this applies: the states below are all scoped to
   `html.js`, which site.js sets before anything paints. A stylesheet that
   hides content and waits for a script to un-hide it is a stylesheet that
   ships a blank page the day the script 404s. */

.js [data-reveal] {
  opacity: 0;
  translate: 0 26px;
  filter: blur(6px);
  transition:
    opacity 0.85s var(--e-soft) var(--d, 0s),
    translate 1.05s var(--e-out) var(--d, 0s),
    scale 1.05s var(--e-out) var(--d, 0s),
    filter 0.85s var(--e-soft) var(--d, 0s);
}
.js [data-reveal].is-in { opacity: 1; translate: none; scale: none; filter: none; }

.js [data-reveal="rise"]  { translate: 0 46px; scale: 0.97; }
.js [data-reveal="left"]  { translate: -34px 12px; }
.js [data-reveal="right"] { translate: 34px 12px; }

/* Split text. The word box is the wrapping unit so a headline still
   breaks between words; the character box carries its own perspective so
   each glyph tips on its own baseline rather than sharing one vanishing
   point with the whole line. */

[data-split] .w { display: inline-block; white-space: nowrap; }
[data-split] .ch { display: inline-block; perspective: 420px; }
[data-split] .ch-i,
[data-split] .w-i { display: inline-block; }

.js [data-split="chars"] .ch-i {
  opacity: 0;
  transform: translate3d(0, 0.52em, 0) rotateX(-82deg) scale(0.92);
  transform-origin: 50% 100%;
  filter: blur(9px);
  transition:
    opacity 0.62s var(--e-soft) calc(var(--i, 0) * 24ms),
    transform 0.95s var(--e-out) calc(var(--i, 0) * 24ms),
    filter 0.62s var(--e-soft) calc(var(--i, 0) * 24ms);
}
.js [data-split="chars"].is-in .ch-i { opacity: 1; transform: none; filter: none; }

.js [data-split="words"] .w {
  overflow: hidden;
  vertical-align: top;
  padding-bottom: 0.16em;
  margin-bottom: -0.16em;
}
.js [data-split="words"] .w-i {
  opacity: 0;
  transform: translate3d(0, 108%, 0) rotate(3.5deg);
  transition:
    opacity 0.6s var(--e-soft) calc(var(--i, 0) * 42ms),
    transform 0.9s var(--e-out) calc(var(--i, 0) * 42ms);
}
.js [data-split="words"].is-in .w-i { opacity: 1; transform: none; }

/* ═══ HERO ═════════════════════════════════════════════════════════ */

.hero { padding: 92px 0 56px; }

/* The landing hero only. The version and archive pages reuse `.hero` for
   their own head, and none of what follows — the height, the centring, the
   layer it stacks over — belongs to a page that is just a heading. */
.hero--lead {
  position: relative;
  display: flex; align-items: center;
  min-height: min(94vh, 1000px);
  padding: 104px 0 76px;
}
.hero--lead .wrap { position: relative; z-index: 3; width: 100%; pointer-events: none; }

.hero-copy { max-width: 880px; margin: 0 auto; text-align: center; pointer-events: auto; }

/* The name, said once, above the sentence — and the two of them are now
   the whole of the hero. The lead paragraph under them said in twenty
   words what the headline says in nine.

   Set in the mono cut, which is what lets the letters cycle without the
   word shivering: every glyph occupies the same box, so a swap changes
   what is drawn and never where anything is. */
.wordmark {
  display: inline-flex; align-items: center; gap: 20px;
  margin: 0 0 34px;
}
.wordmark img {
  width: 54px; height: 54px; display: block;
  filter: drop-shadow(0 0 20px rgba(var(--gold-rgb), 0.45));
}
.wordmark span {
  font-family: var(--font-mono);
  font-size: clamp(21px, 2.1vw, 29px); font-weight: 700; line-height: 1;
  letter-spacing: 0.30em; text-indent: 0.30em; text-transform: uppercase;
  color: var(--ink);
  text-shadow: 0 0 22px rgba(var(--gold-rgb), 0.22);
}
/* one box per letter, so the cycling cannot reflow the word */
.wordmark .cy { display: inline-block; }
/* a letter still looking for itself is lit, and dims as it settles */
.wordmark .cy.is-roll { color: var(--gold); opacity: 0.7; }

.kicker {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 13px; letter-spacing: 0.10em; text-transform: uppercase;
  color: var(--dim-2); font-weight: 700;
  margin: 0 0 24px;
}
.kicker s {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--gold); text-decoration: none;
  box-shadow: 0 0 4px rgba(var(--gold-rgb), 0.9), 0 0 14px rgba(var(--gold-rgb), 0.6);
  animation: pip 3.8s var(--e-soft) infinite;
}
@keyframes pip {
  0%, 62%, 100% { opacity: 1; transform: scale(1); }
  72% { opacity: 0.35; transform: scale(0.8); }
  80% { opacity: 1; transform: scale(1.15); }
}

.hero h1 {
  font-family: var(--font-display);
  font-size: clamp(38px, 4.9vw, 70px);
  line-height: 1.0;
  letter-spacing: -0.028em;
  margin: 0 0 24px;
  font-weight: 700;
  max-width: 15ch;
  text-wrap: balance;
}
/* With the lead gone this is the last line of the hero, and it takes the
   room the paragraph was using. */
/* Two clauses, so two lines. At 78px the first of them ran past the
   column and `text-wrap: balance` did the only thing left to it: broke
   the sentence in the middle of a clause. A slightly smaller setting and
   a wider measure let the break fall where the full stop already is. */
.hero--lead h1 {
  font-size: clamp(40px, 4.9vw, 70px);
  max-width: 24ch; margin-inline: auto; margin-bottom: 0;
}

/* The one place the headline is allowed to be alive: once the letters
   have landed, a slow wave of light walks through them, glyph by glyph,
   on the same clock as the pulse crossing the comb behind. It touches
   text-shadow only, so it cannot fight the reveal transition. */
.js [data-split="chars"].is-in .ch-i {
  animation: filament 11s linear calc(1.5s + var(--i, 0) * 46ms) infinite;
}
@keyframes filament {
  0%, 86%, 100% { text-shadow: none; }
  90% {
    text-shadow:
      0 0 10px rgba(var(--gold-rgb), 0.55),
      0 0 30px rgba(var(--gold-rgb), 0.30);
  }
}

.hero p.lead {
  font-size: clamp(17px, 1.5vw, 20px);
  color: var(--dim-2);
  max-width: var(--measure);
  margin: 0 0 34px;
  text-wrap: balance;
}
.actions { display: flex; flex-wrap: wrap; gap: 16px 26px; align-items: center; }
.hero--lead .actions { justify-content: center; margin-top: 42px; }

/* ── the key ────────────────────────────────────────────────────────
   A ticket: a body carrying the label, a line of perforation, and a stub
   holding the version — which gives the version somewhere that is ABOUT
   the version, rather than a figure appended to a sentence. The object
   says what it is, something torn off and handed over, with no word spent
   on saying so.

   THE NOTCHES ARE CUT, NOT PAINTED. There is no background colour here to
   match: the page behind this is a canvas that moves, and a notch painted
   the shade of the page would be a dark blob travelling over a comb. Two
   radial mask layers, intersected, take a real bite out of each facing
   edge, and the two bites meet at the seam as one hole.

   A mask takes the element's box-shadow with it, so the shadow the whole
   ticket casts is a blurred layer of its own behind both halves. It also
   must not be a `filter` on `.get`: a filtered ancestor becomes a backdrop
   root, and the frosted glass inside would have nothing left to frost.

   Only the stub moves on hover, and it moves AWAY from the pointer rather
   than under it, so the control can never unhover itself and flicker
   between its two states — which is what a lift on the whole button does
   near its own edge. */

.get {
  position: relative;
  display: inline-flex; align-items: stretch;
  color: var(--ink);
  transform: translate3d(var(--mx, 0), var(--my, 0), 0);
}
.get:hover { text-decoration: none; }
/* the shadow the ticket casts, unmasked so it stays a shadow */
.get::before {
  content: ""; position: absolute; z-index: -1;
  inset: 10px 6px -12px 6px; border-radius: 20px;
  background: rgba(0, 0, 0, 0.6);
  filter: blur(22px);
  transition: opacity 0.4s var(--e-soft);
}

/* ── the body ── */
.get-body {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 15px;
  padding: 15px 28px 15px 22px;
  border-radius: 16px 5px 5px 16px;
  border: 1px solid rgba(var(--gold-rgb), 0.32);
  border-right: 0;
  background: rgba(255, 255, 255, 0.055);
  backdrop-filter: blur(16px) saturate(1.3);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
  transition: border-color 0.4s var(--e-soft), color 0.35s var(--e-soft);
  mask:
    radial-gradient(circle 9px at 100% 0, transparent 98%, #000 100%),
    radial-gradient(circle 9px at 100% 100%, transparent 98%, #000 100%);
  mask-composite: intersect;
  -webkit-mask:
    radial-gradient(circle 9px at 100% 0, transparent 98%, #000 100%),
    radial-gradient(circle 9px at 100% 100%, transparent 98%, #000 100%);
  -webkit-mask-composite: source-in;
}
/* the gold, washing across the body */
.get-body::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(96deg, var(--gold), #FFE87A 52%, var(--gold));
  transform: scaleX(0); transform-origin: left center;
  transition: transform 0.55s var(--e-out);
}
/* the hand's own light, over the glass and under the gold */
.get-body::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(180px circle at var(--lx, 50%) var(--ly, 50%),
    rgba(255, 255, 255, 0.16), transparent 65%);
  opacity: 0; transition: opacity 0.35s var(--e-soft);
}
.get-body > * { position: relative; z-index: 1; }

.get:hover .get-body, .get:focus-visible .get-body {
  color: #12101f;
  border-color: rgba(var(--gold-rgb), 0.9);
}
.get:hover .get-body::before, .get:focus-visible .get-body::before { transform: scaleX(1); }
.get:hover .get-body::after { opacity: 1; }
.get:active .get-body { filter: brightness(0.95); }

/* The perforation belongs to the BODY, not to the stub: it marks where
   the tear is, and it has to stay there while the stub comes away. */
.get-perf {
  position: absolute; right: 1px; top: 13px; bottom: 13px; width: 0;
  border-left: 2px dashed rgba(var(--gold-rgb), 0.40);
  transition: border-color 0.4s var(--e-soft);
}
.get:hover .get-perf, .get:focus-visible .get-perf {
  border-left-color: rgba(18, 16, 31, 0.45);
}

.get-mark {
  display: block; width: 26px; height: 26px; flex: 0 0 auto;
  color: var(--gold);
  transition: color 0.35s var(--e-soft);
}
.get-mark svg { display: block; width: 100%; height: 100%; }
.get:hover .get-mark, .get:focus-visible .get-mark { color: #12101f; }

.get-txt { display: block; text-align: left; }
.get-txt b { display: block; font-weight: 700; font-size: 17px; letter-spacing: -0.014em; line-height: 1.28; }
.get-txt i {
  display: flex; align-items: center; gap: 8px;
  font-style: normal; font-size: 12.5px; line-height: 1.45;
  color: var(--dim);
  transition: color 0.35s var(--e-soft);
}
.get:hover .get-txt i, .get:focus-visible .get-txt i { color: rgba(18, 16, 31, 0.66); }
.get-txt i s {
  width: 3px; height: 3px; border-radius: 50%; text-decoration: none;
  background: currentColor; opacity: 0.55;
}
/* A dot between two facts has no business being there when either of them
   is missing — and when the key has become a chooser, both usually are. */
.get-txt .get-hint:empty, .get-txt .get-sz:empty { display: none; }
.get-txt i:has(.get-hint:empty) s,
.get-txt i:has(.get-sz:empty) s { display: none; }

/* ── the stub ── */
.get-stub {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; padding: 15px 20px;
  border-radius: 5px 16px 16px 5px;
  border: 1px solid rgba(var(--gold-rgb), 0.32);
  border-left: 0;
  background: rgba(255, 255, 255, 0.035);
  backdrop-filter: blur(16px) saturate(1.3);
  transition: transform 0.5s var(--e-back), border-color 0.4s var(--e-soft),
              background 0.4s var(--e-soft);
  mask:
    radial-gradient(circle 9px at 0 0, transparent 98%, #000 100%),
    radial-gradient(circle 9px at 0 100%, transparent 98%, #000 100%);
  mask-composite: intersect;
  -webkit-mask:
    radial-gradient(circle 9px at 0 0, transparent 98%, #000 100%),
    radial-gradient(circle 9px at 0 100%, transparent 98%, #000 100%);
  -webkit-mask-composite: source-in;
}
.get-stub em {
  font-style: normal; font-size: 9px; font-weight: 700;
  letter-spacing: 0.20em; text-transform: uppercase;
  color: var(--dim); transition: color 0.35s var(--e-soft);
}
.get-stub b {
  font-family: var(--font-mono);
  font-size: 15px; font-weight: 700; line-height: 1;
  letter-spacing: 0.01em; color: var(--gold);
  text-shadow: 0 0 14px rgba(var(--gold-rgb), 0.35);
}
/* the tear */
.get:hover .get-stub, .get:focus-visible .get-stub {
  transform: translateX(7px) rotate(1.5deg);
  border-color: rgba(var(--gold-rgb), 0.8);
  background: rgba(var(--gold-rgb), 0.09);
}
.get:hover .get-stub em, .get:focus-visible .get-stub em { color: var(--dim-2); }
.get:active .get-stub { transform: translateX(10px) rotate(2.2deg); }

.get-mark.is-swap { animation: swap 0.55s var(--e-back); }
@keyframes swap {
  0%   { opacity: 0; transform: translateY(9px) scale(0.6); }
  100% { opacity: 1; transform: none; }
}

/* when the page has worked out which file is yours, the key says so */
.get.is-picked { animation: picked 0.7s var(--e-back); }
@keyframes picked {
  0%   { scale: 1; }
  40%  { scale: 1.035; }
  100% { scale: 1; }
}

/* ── the midground ───────────────────────────────────────────────
   Screens at several depths, between the comb and the words. The layer
   takes the pointer rotation as one rigid body, so the near plates
   travel past the far ones — which is what makes it depth rather than
   several pictures at several sizes.

   THERE ARE TWO LAYERS, not one, and they are two for a reason that is
   not composition: perspective belongs to the box that declares it, so a
   single stage as tall as the document would put its vanishing point a
   thousand pixels away from the plates at either end and shear them.
   Each stage is about a screen tall and carries its own turn.

   EVERY PLATE IS OUT OF FOCUS UNTIL IT IS LOOKED AT. That is the whole
   interaction: they are objects at a distance, and the distance is real
   enough to blur them, so the headline never competes with a
   screenshot's own grid. Hover pulls one into focus and types out what
   it is. Nothing lifts, nothing lights an outline — a plate that grew a
   coloured border on hover was answering with a widget's vocabulary,
   and these are photographs. */

.mid {
  position: absolute; inset: 0; z-index: 1;
  perspective: 1600px;
  perspective-origin: 50% 44%;
  pointer-events: none;
}
.mid-3d {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
}

.float {
  position: absolute;
  transform-style: preserve-3d;
  transform: translateZ(var(--z, 0px)) rotateX(var(--fx, 0deg)) rotateY(var(--fy, 0deg));
}
.float-in {
  transform-style: preserve-3d;
  animation: drift var(--dur, 15s) var(--e-soft) var(--lag, 0s) infinite alternate;
}
@keyframes drift {
  from { transform: translate3d(0, 0, 0) rotate(0deg); }
  to   { transform: translate3d(var(--dx, 18px), var(--dy, -28px), 0) rotate(var(--dr, 0.6deg)); }
}

.float-plate {
  position: relative;
  display: block; width: 100%; padding: 0; margin: 0;
  appearance: none; border: 1px solid var(--line-2); border-radius: 12px;
  background: #0a0a18; overflow: hidden; cursor: zoom-in;
  pointer-events: auto;
  filter: blur(var(--soft, 2.4px));
  box-shadow:
    0 42px 90px rgba(0, 0, 0, 0.62),
    0 8px 22px rgba(0, 0, 0, 0.5);
  transition: filter 0.55s var(--e-soft);
}
.float-plate img { display: block; width: 100%; height: auto; }
/* the sheet of glass over the screen */
.float-plate::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(148deg, rgba(255, 255, 255, 0.13), transparent 46%);
}
.float-plate:hover, .float-plate:focus-visible { filter: blur(0px); }

/* The caption is a CHILD of its own plate's 3D box, lifted in front of
   it — so it turns with the picture it names, and the two can never come
   apart the way a caption in a column beside a stage can.

   It does not exist until the plate is looked at. The sentence is then
   typed in by site.js rather than faded in, because the plate coming
   into focus and a line being written are the same gesture: something
   is being paid attention to. */
.float-cap {
  margin: 18px 0 0; padding: 0 2px;
  transform: translateZ(26px);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 10px;
  text-shadow: 0 2px 14px rgba(3, 3, 10, 0.9);
  opacity: 0; pointer-events: none;
  transition: opacity 0.3s var(--e-soft);
}
/* Hover alone shows it, and as a SIBLING selector rather than through an
   ancestor that is deliberately not a hit target. That is the whole
   no-script story here: with no script the sentence was never taken out
   of the document, so hovering simply reveals it, written already.
   The is-lit class is what site.js adds on top, to type it instead. */
.float-plate:hover ~ .float-cap,
.float-plate:focus-visible ~ .float-cap,
.float.is-lit .float-cap { opacity: 1; }
.float-cap s {
  width: 6px; height: 6px; border-radius: 50%; text-decoration: none;
  background: var(--tone); align-self: center;
  box-shadow: 0 0 10px rgba(var(--tone-rgb), 0.85);
}
.float-cap b {
  font-family: var(--font-mono);
  font-size: 13px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--tone);
}
/* Set to be read, not to be captioned. At fourteen pixels under a plate
   drawn at four hundred it was a footnote to a picture; the sentence is
   the half of this that says anything, and the plate is the illustration. */
.float-cap i {
  flex: 1 1 100%; min-height: 1.3em;
  font-family: var(--font-display);
  font-style: normal;
  font-size: clamp(19px, 1.6vw, 28px); line-height: 1.24;
  letter-spacing: -0.015em;
  color: var(--ink);
}
/* the caret, while there is still something to write */
.float.is-lit .float-cap i::after {
  content: ""; display: inline-block;
  width: 3px; height: 0.92em; margin-left: 4px; vertical-align: -0.1em;
  background: var(--tone);
  box-shadow: 0 0 8px rgba(var(--tone-rgb), 0.8);
  animation: caret 0.9s steps(1) infinite;
}
.float.is-typed .float-cap i::after { display: none; }
@keyframes caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* ── the slots ─────────────────────────────────────────────────────
   A place a plate can stand, not a plate. Percentages of the stage box
   and vw for the widths, so the composition keeps its shape rather than
   its pixels. `--soft` is the resting blur and `--far` the resting
   opacity; both are depth cues as much as focus ones.

   The names are the contract site.js works from, and the SIDE IS IN THE
   NAME on purpose: `hs-l2` is the hero's second left-hand slot. The rule
   "never all three on the same side" then reads off the class the plate
   is wearing, with no second table to keep in step with this one.

   NOTHING IS IN THE MIDDLE. Every slot lives in a gutter — a left slot's
   inner edge stops around 27vw, a right slot's around 71vw — and the copy
   column is 880px centred. That is not a nicety now that the positions
   are drawn at random: a plate that CAN land under the headline will,
   eventually, on somebody's screen. */

/* the hero, left */
.hs-l1 { left: 0.5vw;  top: 6%;    width: 27vw; --z: -180px; --far: 1;    --fy: 15deg; --fx: 2deg;  --soft: 2.2px; --dur: 15s; --dx: 16px; --dy: -30px; --dr: 0.5deg; }
.hs-l2 { left: -3vw;   top: 36%;   width: 24vw; --z: -290px; --far: 0.9;  --fy: 17deg; --fx: -1deg; --soft: 2.7px; --dur: 19s; --dx: 20px; --dy: 22px;  --dr: -0.5deg; --lag: -6s; }
.hs-l3 { left: -2.5vw; bottom: 4%; width: 23vw; --z: -360px; --far: 0.88; --fy: 13deg; --fx: -3deg; --soft: 3.0px; --dur: 20s; --dx: 24px; --dy: 26px;  --dr: -0.7deg; --lag: -7s; }
/* the hero, right */
.hs-r1 { right: 0vw;    top: 4%;    width: 29vw; --z: -135px; --far: 1;    --fy: -16deg; --fx: 3deg;  --soft: 2.0px; --dur: 18s; --dx: -20px; --dy: -24px; --dr: -0.6deg; --lag: -4s; }
.hs-r2 { right: -3.5vw; top: 37%;   width: 25vw; --z: -270px; --far: 0.9;  --fy: -18deg; --fx: 1deg;  --soft: 2.6px; --dur: 22s; --dx: -22px; --dy: 20px;  --dr: 0.6deg; --lag: -11s; }
.hs-r3 { right: -2vw;   bottom: 2%; width: 25vw; --z: -320px; --far: 0.88; --fy: -12deg; --fx: -2deg; --soft: 2.8px; --dur: 16s; --dx: -18px; --dy: 30px;  --dr: 0.8deg; --lag: -2s; }

/* The low stage, over a band about three hundred pixels tall, so both of
   its slots hang out of it on purpose — one up into the foot of the hero,
   one down into the footer — which is what carries the layer across the
   seam rather than parking it inside one band. Whichever is occupied is
   seen largely THROUGH the release cards, whose glass finally has
   something behind it other than comb. */
.ls-l { left: -1vw;  top: -30%;    width: 22vw; --z: -300px; --far: 0.86; --fy: 14deg;  --fx: -3deg; --soft: 2.6px; --dur: 21s; --dx: 18px;  --dy: 24px;  --dr: -0.6deg; --lag: -3s; }
.ls-r { right: -2vw; bottom: -22%; width: 24vw; --z: -250px; --far: 0.86; --fy: -15deg; --fx: 3deg;  --soft: 2.4px; --dur: 19s; --dx: -20px; --dy: -22px; --dr: 0.7deg; --lag: -8s; }

/* Distance, as the eye reads it. It is a property of the SLOT, not of the
   plate standing in it — the plates move now — and it is set on
   `.float-in` rather than on the float itself, because the float owns
   coming and going and an opacity there would have to be `1`, which is to
   say no distance at all. */
.float-in { opacity: var(--far, 1); }

/* Where that stage sits in the stack. Three rules place it, and the
   middle one is the one that is easy to get wrong:

   `z-index: 0` rather than the hero stage's 1, so it is the further of
   the two where they meet in the hero's foot. The wrap holding the cards
   is then raised over it, since a positioned element at z-index auto
   loses to a z-index of 0 on the step, not on tree order — without this
   the plates float in front of the release notes. And the band's own
   hairline is raised with it, because the plate crosses the top of the
   band and a rule that says where a section begins should not be the
   thing behind the picture.

   It is NOT -1, which would have been tidier and is a trap: `main` is a
   stacking context, so -1 paints the layer behind main's own box — and a
   transparent box still takes the pointer, so the plates would have been
   there to look at and impossible to hover. */
.mid--low { z-index: 0; }
.band--versions > .wrap { position: relative; z-index: 1; }
.band--versions::before { z-index: 1; }

/* The entry. Opacity and `translate` only — `transform` is where a plate
   IS, and the layer above is what carries `data-reveal`. */
.js .mid .float { opacity: 0; translate: 0 38px; }
.js .mid .float {
  transition: opacity 0.9s var(--e-soft) var(--d, 0ms),
              translate 1.15s var(--e-out) var(--d, 0ms);
}
.js .mid.is-in .float { opacity: 1; translate: none; }

/* ── three at a time ───────────────────────────────────────────────
   `is-rota` is site.js saying it has taken the cast over: from here a
   plate is on the page only while it holds a seat, and it arrives and
   leaves rather than simply being there. The class is added by the
   script and only when it will actually run the rotation — never with no
   script, never under reduced motion, never below 980px where the layer
   is a flow gallery and every screen should show — so the fallback in
   all three cases is the whole cast, standing still, which is what the
   rest of this stylesheet already draws.

   It comes and goes on `opacity` and `scale`. `scale` is the independent
   property, NOT a transform: `transform` on a float is where the plate
   stands in the 3D box, and writing a fade into it would flatten the
   stage. And it scales UP into place from slightly small, which is the
   difference between a picture arriving from further back and a picture
   being switched on. `:not(.is-on)` rather than a bare rule so this
   outranks the entry reveal above it, whatever their order. */
.js .mid.is-rota .float {
  transition: opacity 0.85s var(--e-soft), scale 1.2s var(--e-out);
}
.js .mid.is-rota .float:not(.is-on) {
  opacity: 0; translate: none; scale: 0.93; pointer-events: none;
}
.js .mid.is-rota .float:not(.is-on) .float-plate { pointer-events: none; }
.js .mid.is-rota .float.is-on { opacity: 1; translate: none; scale: 1; }

/* The clearing. The plates pass under the sentence at some widths, and
   this is what keeps it readable at every one of them — depth of field,
   doing a job. */
.hero-veil {
  position: absolute; inset: -6% -4%; z-index: 2; pointer-events: none;
  background: radial-gradient(46% 44% at 50% 46%,
    rgba(5, 5, 15, 0.97) 0%,
    rgba(5, 5, 15, 0.90) 38%,
    rgba(5, 5, 15, 0.62) 60%,
    rgba(5, 5, 15, 0.20) 78%,
    transparent 92%);
}

/* ═══ THE CLAIMS ═══════════════════════════════════════════════
   Three bordered pillars in a band of their own, for three short claims.
   The claims have not changed; the furniture around them has gone. */

.claims {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 10px 22px;
  margin: 30px 0 0; padding-top: 22px;
  border-top: 1px solid var(--line);
  color: var(--dim); font-size: 14.5px;
}
.hero--lead .claims {
  position: relative;
  justify-content: center; max-width: 920px; margin: 46px auto 0; padding-top: 26px;
  gap: 10px 30px;
  border-top: 0;
  text-shadow: 0 2px 14px rgba(3, 3, 10, 0.95);
}
.hero--lead .claims::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--line) 26%, var(--line) 74%, transparent);
}
.claims span { display: inline-flex; align-items: center; gap: 10px; }
.claims span::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--gold); opacity: 0.75;
  box-shadow: 0 0 8px rgba(var(--gold-rgb), 0.7);
}

/* ═══ TONES ════════════════════════════════════════════════════════
   The app's own vocabulary: gold is the accent the planning grid runs
   on, teal is revenus, pink is dépenses, blue is épargne. Anything that
   is ABOUT one of those screens wears its colour, and gets it from here
   rather than restating the rgb triplet. */

.tone--gold { --tone: var(--gold); --tone-rgb: var(--gold-rgb); }
.tone--teal { --tone: var(--teal); --tone-rgb: var(--teal-rgb); }
.tone--pink { --tone: var(--pink); --tone-rgb: var(--pink-rgb); }
.tone--blue { --tone: var(--blue); --tone-rgb: var(--blue-rgb); }

/* ═══ THE PANELS THAT EXPAND ═══════════════════════════════════════
   Two things used to interrupt the page to say something small. The
   other systems were a jump to a section further down — a visitor who
   is not on Windows had to leave the sentence they were reading to
   find out what else there was — and the receipt that follows a
   download was a dialog over the whole window, which is a lot of
   ceremony for "here is what your OS is about to say".

   Both are the same object now: a panel that grows out of the thing
   that was clicked. One opens under the two hero controls, one under
   the file list, and only ever one of them at a time.

   `grid-template-rows: 0fr → 1fr` is what animates, so the panel is
   exactly as tall as whatever it is given and nothing has to measure
   anything. The `visibility` hop is not decoration either: a collapsed
   panel still holds focusable links, and Tab must not walk into a
   list nobody can see. */

.slot { margin: 0; }

.drawer {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.55s var(--e-out);
}
.drawer-box {
  overflow: hidden; min-height: 0;
  visibility: hidden; transition: visibility 0s linear 0.55s;
}
.drawer.is-open { grid-template-rows: 1fr; }
.drawer.is-open .drawer-box { visibility: visible; transition-delay: 0s; }
/* The platform list is rendered into the document, inside a panel. With no
   script there is nothing to expand it, so it is never collapsed: the six
   files sit open under the key, which is where somebody choosing one
   already is. Its close button is the only part that needs a script. */
html:not(.js) .drawer { grid-template-rows: 1fr; }
html:not(.js) .drawer-box { visibility: visible; }
html:not(.js) .drawer-in { opacity: 1; translate: none; }
html:not(.js) .drawer-x { display: none; }

/* The panel IS a pane — the same glass, and the same light under the
   pointer as the release cards below it. It carries .pane, so what is
   left here is only what makes it a drawer: where it sits, and how it
   arrives. */
.drawer-in {
  margin: 24px 0 0;
  text-align: left;
  opacity: 0; translate: 0 -12px;
  transition: opacity 0.4s var(--e-soft) 0.06s, translate 0.5s var(--e-out) 0.06s;
}
.drawer.is-open .drawer-in { opacity: 1; translate: none; }
/* the seam it grew out of */
.drawer-in::before {
  content: ""; position: absolute; left: 26px; right: 26px; top: -1px; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(var(--gold-rgb), 0.55), transparent);
}

.drawer-slot { animation: drawer-cut 0.4s var(--e-soft); }
@keyframes drawer-cut {
  from { opacity: 0; transform: translateY(-5px); }
  to   { opacity: 1; transform: none; }
}

.drawer-head {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 16px; padding-right: 40px;
  font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--gold);
}
.drawer-head s {
  width: 7px; height: 7px; border-radius: 50%; background: var(--gold);
  text-decoration: none; flex: 0 0 auto;
  box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.9);
  animation: pip 2.4s ease-in-out infinite;
}

.drawer-in > .drawer-x {
  position: absolute; right: 13px; top: 13px; z-index: 2;
  appearance: none; border: 1px solid var(--line); background: rgba(255, 255, 255, 0.04);
  color: var(--dim-2); cursor: pointer;
  width: 32px; height: 32px; border-radius: 10px;
  display: grid; place-items: center;
  transition: color 0.3s var(--e-soft), border-color 0.3s var(--e-soft);
}
.drawer-in > .drawer-x svg { width: 16px; height: 16px; }
.drawer-in > .drawer-x:hover { color: var(--ink); border-color: var(--line-2); }

/* ── the receipt ───────────────────────────────────────────────────
   What the visitor was just handed, and what their own system is
   about to say about it. Two columns where there is room: the steps
   on the left, the things to know on the right. */

.drawer h3 {
  margin: 0 0 6px; font-size: 21px; line-height: 1.2;
  letter-spacing: -0.022em; font-weight: 700;
}
.drawer-file { margin: 0 0 18px; color: var(--dim); font-size: 13.5px; }
.drawer-file code {
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 12.5px; color: var(--dim-2);
}
/* One column, with the asides under it. Two columns were right when the
   instructions were three lines; they are now three paragraphs and a
   drawing, and a short right-hand column beside a long left-hand one is
   a page with a hole in it. */
.drawer-body { display: grid; gap: 18px; }
.drawer-asides {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: start;
}
/* the install card, stripped of its card: in here it is the only thing
   being said, so a border round it is a box inside a box */
/* Nothing inside a panel is a panel. The install block keeps .pane in the
   document, where it is one of three cards standing on the page and needs
   a surface of its own; lifted in here it gives that surface up and the
   words sit directly on the drawer's own glass. */
.drawer-slot .pane {
  border: 0; background: none; backdrop-filter: none;
  box-shadow: none; padding: 0; border-radius: 0;
}
.drawer-slot .pane::before { display: none; }
.drawer .card { margin: 0; }
.drawer .card h3 { display: none; }
.drawer .card p { margin: 0 0 14px; font-size: 15.5px; color: var(--dim-2); }
.drawer .card p:last-child { margin-bottom: 0; }
.drawer .card ul { font-size: 15.5px; color: var(--dim-2); }
.drawer .told { margin: 0 0 12px; }
.drawer-note { margin: 0; color: var(--dim); font-size: 13.5px; }
.drawer-note b { color: var(--dim-2); }

/* ═══ PLAIN SECTIONS ═══════════════════════════════════════════════ */

.band { padding: 84px 0; position: relative; }
/* No heading over it, so no room reserved for one. The foot is deeper
   than the standard band because the low midground stands in it: the
   cards used to end eighty pixels above the footer rule, which is not
   enough room for anything to be behind them. */
.band--versions { padding-top: 24px; padding-bottom: 132px; }
.band::before {
  content: ""; position: absolute; top: 0; left: 8%; right: 8%; height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-2) 22%, var(--line-2) 78%, transparent);
}
.band h2 {
  font-size: clamp(27px, 3.1vw, 38px); line-height: 1.12;
  letter-spacing: -0.028em;
  margin: 0 0 16px; font-weight: 700; text-wrap: balance; max-width: 20ch;
}
.band .intro { color: var(--dim); max-width: var(--measure); margin: 0 0 38px; }

/* ═══ THE PANE ═════════════════════════════════════════════════════
   One register, used by everything on this page that is a sheet of
   glass holding one thing to read: the release card, and each of the
   install blocks. A pane lights softly under the pointer and does
   nothing else — no border that changes colour, no lift, no glow of
   its own sitting there being lit at nobody. */

.pane {
  position: relative; overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.13); border-radius: 18px;
  background: linear-gradient(158deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.022));
  backdrop-filter: blur(22px) saturate(1.35);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 26px 70px rgba(0, 0, 0, 0.42);
  padding: 26px 30px 28px;
}
.pane::before {
  content: ""; position: absolute; inset: 0; opacity: 0;
  background: radial-gradient(340px circle at var(--lx, 50%) var(--ly, 50%),
    rgba(var(--gold-rgb), 0.12), transparent 70%);
  transition: opacity 0.4s var(--e-soft);
}
.pane:hover::before { opacity: 1; }
.pane > * { position: relative; }

/* ── what Windows is about to say ──────────────────────────────────
   Hovering the word draws the dialog. The word is the control, so it is
   set as one — gold, bold, on a dashed rule, in a page where nothing
   else inside a paragraph is.

   CLOSED AT REST, not reserved and empty: a dashed rectangle sitting in
   the middle of a paragraph reads as a picture that failed to load,
   which is the one thing this must not look like. The space opens
   BELOW the word, so nothing moves under the pointer while it does.

   Four stages, and they are four transition delays rather than an
   animation, so letting go reverses them instead of snapping: the space
   opens, the frame strokes itself round, the blue washes down, and the
   words land in reading order. The whole thing is `:has()` — no script
   is involved, which matters because this block is also the entire
   no-script install section. */

.peek-wrap { position: relative; }

.peek {
  appearance: none; border: 0; padding: 0 1px; margin: 0;
  background: none; cursor: help;
  font: inherit; font-weight: 700;
  color: var(--gold);
  border-bottom: 2px dashed rgba(var(--gold-rgb), 0.55);
  text-shadow: 0 0 12px rgba(var(--gold-rgb), 0.35);
  transition: color 0.3s var(--e-soft), border-color 0.3s var(--e-soft),
              text-shadow 0.3s var(--e-soft);
}
.peek:hover, .peek:focus {
  color: #FFF0A0;
  border-bottom-color: rgba(var(--gold-rgb), 1);
  text-shadow: 0 0 6px rgba(var(--gold-rgb), 0.9), 0 0 22px rgba(var(--gold-rgb), 0.5);
  outline: none;
}

.smart {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.34s var(--e-out);
}
.smart-box { overflow: hidden; min-height: 0; }
.smart-pane {
  position: relative;
  width: min(100%, 420px);
  margin: 16px 0 6px;
  padding: 20px 22px 18px;
  color: #fff;
}

/* the frame, strokes drawn round from the left */
.smart-draw {
  position: absolute; inset: 0; border-radius: 3px;
  border: 1.4px solid rgba(var(--blue-rgb), 0.95);
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.4s var(--e-out);
}
/* the blue, washing down inside it */
.smart-fill {
  position: absolute; inset: 1px; border-radius: 2px;
  background: linear-gradient(150deg, #1d5fa8, #16447a);
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.3s var(--e-out);
}
.smart-in { position: relative; }
.smart-in > * {
  margin: 0; opacity: 0; translate: 0 7px;
  transition: opacity 0.24s var(--e-soft), translate 0.3s var(--e-out);
}
.smart-title {
  font-family: var(--font-display);
  font-size: 19px; font-weight: 700; line-height: 1.2; letter-spacing: -0.01em;
  margin-bottom: 10px !important;
}
.smart-text { font-size: 13px; line-height: 1.5; color: rgba(255, 255, 255, 0.88); }
.smart-more {
  font-size: 13px; margin-top: 12px !important;
  color: #fff; text-decoration: underline; text-underline-offset: 3px;
}
.smart-btns { margin-top: 16px !important; display: flex; justify-content: flex-end; }
.smart-btn {
  display: inline-block; padding: 6px 18px;
  font-size: 12.5px; color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.75);
}

/* ── and what happens when the word is looked at ── */
.peek-wrap:has(.peek:hover) .smart,
.peek-wrap:has(.peek:focus) .smart { grid-template-rows: 1fr; }

.peek-wrap:has(.peek:hover) .smart-draw,
.peek-wrap:has(.peek:focus) .smart-draw {
  clip-path: inset(0);
  transition-delay: 0.2s;
}
.peek-wrap:has(.peek:hover) .smart-fill,
.peek-wrap:has(.peek:focus) .smart-fill {
  clip-path: inset(0);
  transition-delay: 0.54s;
}
.peek-wrap:has(.peek:hover) .smart-in > *,
.peek-wrap:has(.peek:focus) .smart-in > * { opacity: 1; translate: none; }
.peek-wrap:has(.peek:hover) .smart-title,
.peek-wrap:has(.peek:focus) .smart-title { transition-delay: 0.76s; }
.peek-wrap:has(.peek:hover) .smart-text,
.peek-wrap:has(.peek:focus) .smart-text { transition-delay: 0.84s; }
.peek-wrap:has(.peek:hover) .smart-more,
.peek-wrap:has(.peek:focus) .smart-more { transition-delay: 0.92s; }
.peek-wrap:has(.peek:hover) .smart-btns,
.peek-wrap:has(.peek:focus) .smart-btns { transition-delay: 1s; }

/* ── install cards ─────────────────────────────────────────────── */

.cards { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); }
/* The surface, the border and the light all belong to `.pane` now — an
   install block and the release card are the same kind of object, and
   two recipes for it is two things to keep in step. What is left here is
   what the card SAYS. */
.card h3 { margin: 0 0 13px; font-size: 17px; letter-spacing: -0.01em; }
.card p, .card ul { margin: 0; color: var(--dim-2); font-size: 15.5px; }
.card ul { padding-left: 20px; }
.card li + li { margin-top: 9px; }

code {
  background: rgba(0, 0, 0, 0.45); border: 1px solid var(--line); border-radius: 6px;
  padding: 2px 8px; font-size: 13.5px; color: var(--gold);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  overflow-wrap: anywhere;
}

/* ── the told-you notes ────────────────────────────────────────── */

.told {
  position: relative;
  border-left: 3px solid var(--gold);
  background: linear-gradient(90deg, rgba(var(--gold-rgb), 0.09), rgba(var(--gold-rgb), 0.02) 60%, transparent);
  padding: 19px 26px; border-radius: 0 14px 14px 0;
  color: var(--dim-2);
}
/* No glow on the rule. This note sits beside the install block, and a lit
   edge there was the one thing in that panel asking to be looked at while
   the panel was answering a different question. */
.told::before {
  content: ""; position: absolute; left: -3px; top: 0; bottom: 0; width: 3px;
  background: rgba(var(--gold-rgb), 0.75);
}
.told + .told { margin-top: 12px; }
.told b { color: var(--gold); }
.told--warn { border-left-color: var(--pink); background: linear-gradient(90deg, rgba(var(--pink-rgb), 0.10), rgba(var(--pink-rgb), 0.02) 60%, transparent); }
.told--warn::before { background: var(--pink); box-shadow: 0 0 14px rgba(var(--pink-rgb), 0.8); }
.told--warn b { color: var(--pink); }

/* ── the instructions, at rest ─────────────────────────────────────
   There is no dialog on this page any more. What is left of the thing
   that used to open one is this: #installer is a real section of the
   document, it is where the drawer lifts its text from, and it is the
   whole install story on a page with no script at all. .js is what hides
   it, so a failed script load costs the timing and not the information. */

.js #installer { display: none; }

/* ── release notes ─────────────────────────────────────────────── */

.notes .summary { font-size: 19px; color: var(--ink); margin: 0 0 30px; max-width: var(--measure); }
.notes h3 {
  font-size: 13px; margin: 34px 0 14px; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--gold);
}
/* No measure cap and no single column. The list is inside a card that
   spans the whole row when it opens, and a 46rem column down the left of
   it left the other half of the card empty while the list ran for three
   screens. Two columns use the card and halve its height, and the measure
   is then set by the column rather than by a number that has to be kept
   in step with the card by hand. `break-inside` is what stops an item
   being cut across the gutter — the bullet is positioned against the
   item, and half an item has half a bullet. */
.notes ul {
  margin: 0; padding-left: 0; list-style: none; color: var(--dim-2);
  columns: 2; column-gap: 44px;
}
.notes li {
  position: relative; margin-bottom: 7px; padding-left: 22px;
  break-inside: avoid; -webkit-column-break-inside: avoid;
}
.notes li::before {
  content: ""; position: absolute; left: 4px; top: 0.62em;
  width: 5px; height: 5px; border-radius: 50%;
  background: rgba(var(--gold-rgb), 0.55);
}
.notes li b { color: var(--ink); }
.notes li em { color: var(--ink); font-style: italic; }
.section-intro { color: var(--dim); }

/* ── the other systems ──────────────────────────────────────────────
   Six marks in a row under the ticket, one per file, and each one IS its
   download. Hovering raises a card under it with the name, who the file
   is for and how big it is.

   It replaced a second button that opened a panel holding the same six
   rows. The button spent a click asking a question the page can simply
   answer: somebody on a Mac needs to SEE that there is a Mac file, not
   be told there is a list with one in it.

   The card is absolutely positioned, so nothing in the row moves when it
   appears — a row that reflows under the pointer is a row you cannot aim
   at. And the mark is the LINK, never a trigger for one, which is what
   makes this work with no script and from a keyboard alike.

   THE ROW HAS TO BE RAISED, and the raising cannot live on the card. Each
   mark carries `backdrop-filter`, which makes it a stacking context, so
   the card's own `z-index: 5` never leaves the 52px box it is drawn in —
   it orders the card against its sibling icon and against nothing else.
   The panel that opens under this row is `.pane`, also a stacking context,
   also z-index auto, and later in the document: so once a download had
   started, every name in this row opened UNDER the receipt and could not
   be read. Lifting the whole list is what answers that, because the whole
   list is what the browser is really ordering. */

.alt-list {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 10px;
  margin: 26px 0 0;
  position: relative; z-index: 4;
}

.alt-os {
  position: relative;
  display: grid; place-items: center;
  width: 52px; height: 52px; border-radius: 15px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.04);
  backdrop-filter: blur(14px) saturate(1.2);
  color: var(--dim-2);
  transition: color 0.3s var(--e-soft), border-color 0.3s var(--e-soft),
              background 0.3s var(--e-soft), transform 0.35s var(--e-back);
}
.alt-os[hidden] { display: none; }
.alt-os:hover, .alt-os:focus-visible {
  color: var(--gold);
  border-color: rgba(var(--gold-rgb), 0.65);
  background: rgba(var(--gold-rgb), 0.09);
  transform: translateY(-3px);
  text-decoration: none;
}
.alt-ico { display: block; width: 25px; height: 25px; }
.alt-ico svg { display: block; width: 100%; height: 100%; }

/* the one the page worked out could have been yours, but was not chosen */
.alt-os.is-yours {
  order: -1;
  color: var(--gold);
  border-color: rgba(var(--gold-rgb), 0.5);
  background: rgba(var(--gold-rgb), 0.10);
  box-shadow: 0 0 26px rgba(var(--gold-rgb), 0.18);
}

.alt-card {
  position: absolute; top: calc(100% + 10px); left: 50%;
  transform: translateX(-50%) translateY(-6px);
  display: block; width: max-content; max-width: 240px;
  padding: 10px 14px; border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: linear-gradient(158deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.04));
  backdrop-filter: blur(22px) saturate(1.35);
  box-shadow: 0 20px 46px rgba(0, 0, 0, 0.5);
  text-align: center;
  opacity: 0; pointer-events: none; z-index: 5;
  transition: opacity 0.25s var(--e-soft), transform 0.35s var(--e-out);
}
.alt-card b {
  display: block; font-size: 14px; font-weight: 700; line-height: 1.3;
  letter-spacing: -0.01em; color: var(--ink);
}
.alt-card i {
  display: block; font-style: normal; font-size: 12px; line-height: 1.45;
  color: var(--dim); white-space: nowrap;
}
.alt-os:hover .alt-card, .alt-os:focus-visible .alt-card {
  opacity: 1; transform: translateX(-50%) translateY(0);
}

/* ── the versions ───────────────────────────────────────────────────
   One card per release, side by side, opening where it stands. There
   used to be a page per version and an archive listing them; a release
   note is four lines, and a navigation away from the only thing on this
   site anybody came for is a poor price for four lines.

   Every card but the latest carries its own six files as marks. The
   latest carries none: the ticket at the top of the page is its
   download, and a second one here is the same offer made twice. */

.vgrid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: stretch;
}

.vcard { padding: 0; }
.vcard.is-open { grid-column: 1 / -1; }

/* A grid, not a wrapping flex row. The card is 300px wide in the row and
   the whole width of the page once it is open, and flex-wrap answers that
   by putting the summary beside the version number at one of those widths
   and under it at the other. The rows here are the rows at every width. */
.vcard-head {
  appearance: none; width: 100%; border: 0; background: none;
  display: grid; grid-template-columns: auto 1fr; align-items: baseline;
  gap: 6px 14px; padding: 22px 26px 20px; margin: 0;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.vcard-num { grid-column: 1; }
.vcard-date { grid-column: 2; justify-self: start; }
.vcard-kick, .vcard-sum, .vcard-go { grid-column: 1 / -1; }
.vcard-kick {
  display: flex; align-items: center; gap: 9px;
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--dim);
}
.vcard--now .vcard-kick { color: var(--gold); }
.vcard-kick s {
  width: 7px; height: 7px; border-radius: 50%; text-decoration: none;
  background: var(--dim); flex: 0 0 auto;
}
.vcard--now .vcard-kick s {
  background: var(--gold);
  box-shadow: 0 0 10px rgba(var(--gold-rgb), 0.9);
  animation: pip 3.8s var(--e-soft) infinite;
}
.vcard-num {
  font-family: var(--font-mono);
  font-size: 27px; font-weight: 700; line-height: 1; letter-spacing: -0.01em;
  color: var(--ink);
}
.vcard-date { font-size: 14px; color: var(--dim); }
.vcard-sum {
  margin-top: 8px; max-width: 64ch;
  font-size: 15px; line-height: 1.55; color: var(--dim-2);
}
.vcard-go {
  display: inline-flex; align-items: center; gap: 7px; justify-self: start;
  margin-top: 14px;
  font-size: 14px; font-weight: 700; color: var(--gold);
}
.vcard-go svg {
  width: 16px; height: 16px; display: block;
  transition: transform 0.4s var(--e-out);
}
.vcard-head:hover .vcard-go svg { transform: translateY(2px); }
.vcard.is-open .vcard-go svg { transform: rotate(180deg); }

/* the files an older version carries, in the collapsed card */
.vfiles {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 0 26px 22px;
}
.vfile {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: 11px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.03);
  color: var(--dim);
  transition: color 0.3s var(--e-soft), border-color 0.3s var(--e-soft),
              background 0.3s var(--e-soft);
}
.vfile svg { display: block; width: 19px; height: 19px; }
.vfile:hover, .vfile:focus-visible {
  color: var(--gold);
  border-color: rgba(var(--gold-rgb), 0.55);
  background: rgba(var(--gold-rgb), 0.08);
  text-decoration: none;
}

.vcard-log {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s var(--e-out);
}
.vcard-log-in {
  overflow: hidden; min-height: 0;
  visibility: hidden; transition: visibility 0s linear 0.5s;
}
.vcard.is-open .vcard-log { grid-template-rows: 1fr; }
.vcard.is-open .vcard-log-in { visibility: visible; transition-delay: 0s; }
.vcard-log .notes { padding: 4px 26px 26px; }
.vcard-log .notes h3:first-child { margin-top: 0; }

/* with no script there is nothing to open them, so nothing is closed */
html:not(.js) .vcard-log { grid-template-rows: 1fr; }
html:not(.js) .vcard-log-in { visibility: visible; }
html:not(.js) .vcard-go { display: none; }

/* ═══ FOOTER ═══════════════════════════════════════════════════════ */

footer {
  position: relative;
  padding: 56px 0 84px;
  color: var(--dim); font-size: 15px;
}
footer::before {
  content: ""; position: absolute; top: 0; left: 8%; right: 8%; height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-2) 22%, var(--line-2) 78%, transparent);
}
/* Centred, because there is one thing in it. `space-between` was right
   when the footer held a note on the left and a row of links on the
   right; with the links gone it justified a single paragraph hard left
   under a page whose every other line is centred on the measure. */
footer .wrap { display: flex; gap: 34px; flex-wrap: wrap; justify-content: center; }
footer p { margin: 0; max-width: 62ch; text-align: center; text-wrap: pretty; }
footer b { color: var(--ink); }

/* ═══ NARROWER ═════════════════════════════════════════════════════
   Under 1280px the plates would start crossing the sentence rather than
   passing behind its edges, so they step outward and shrink before the
   clearing has to work for a living. */

@media (max-width: 1279px) {
  .hs-l1 { left: -2vw;   width: 28vw; }
  .hs-l2 { left: -5vw;   width: 25vw; }
  .hs-l3 { left: -4vw;   width: 24vw; }
  .hs-r1 { right: -2.5vw; width: 30vw; }
  .hs-r2 { right: -5.5vw; width: 26vw; }
  .hs-r3 { right: -3.5vw; width: 26vw; }
  /* The low stage steps outward too — there is a release card in the
     middle of it, and at this width the card is most of the band. */
  .ls-l { left: -6vw;  width: 24vw; }
  .ls-r { right: -7vw; width: 26vw; }
}

/* ═══ THE MIDGROUND STOPS BEING ONE ════════════════════════════════
   Below the point where there is room beside the sentence, floating the
   plates behind it is not depth any more — it is a screenshot under a
   headline. So the layer stops being a layer: the same four plates fall
   into the flow underneath the copy, two up, captions and all, and the
   whole 3D apparatus stands down rather than being crammed.

   It is the same markup and the same four pictures. Only the geometry
   gives up, which is the one thing at these widths that has to. */

@media (max-width: 979px) {

  .hero--lead {
    display: flex; flex-direction: column; align-items: stretch;
    min-height: 0; padding: 64px 0 44px;
  }
  .hero--lead .wrap { order: 1; }
  .hero-veil { display: none; }

  .mid {
    order: 2; position: static; perspective: none; pointer-events: auto;
    max-width: 1180px; margin: 52px auto 0; padding: 0 30px;
  }
  .mid-3d {
    position: static; transform: none !important;
    display: grid; grid-template-columns: 1fr 1fr; gap: 26px 24px;
  }
  .float {
    position: static; width: auto !important;
    transform: none !important;
  }
  .float-in { animation: none; opacity: 1 !important; }
  /* No pointer, no hover, so neither the focus nor the caption can be a
     reward for one: every plate is sharp and every caption is written out.
     site.js leaves the text alone at these widths for the same reason. */
  .float-plate { filter: none !important; box-shadow: 0 22px 50px rgba(0, 0, 0, 0.5); }
  .float-cap { transform: none; opacity: 1 !important; }
  .float .float-cap i::after { display: none !important; }
  /* The low stage falls into the flow the same way, under the release
     cards rather than behind them — which at these widths is simply two
     more screens at the end of the page, where there were none. */
  .mid--low { margin-top: 44px; }
  .band--versions { padding-bottom: 60px; }

  /* The marks do NOT stand down here. They used to: with no pointer to
     hover, the card that names each file can never open, so the block
     unwound into a column of full-width rows carrying the names — which
     is exactly the stack of download buttons this page was rebuilt to
     stop being. Reproducing the old control at the widths where most
     visitors arrive gave the page two identities, and the narrow one was
     the discarded one.

     So the row survives whole, and the card simply stays shut. What is
     lost is the visible name and size on touch; what is kept is that the
     page looks like itself. The name is not gone — `aria-label` on each
     mark carries the platform and the size, so the information is still
     announced, and the ticket above already names the file the visitor
     most likely wants. Below 640px the gap tightens (see there) and five
     52px marks sit in one row inside the 20px gutters. */
}

@media (max-width: 640px) {
  body { font-size: 16px; }
  .wrap { padding: 0 20px; }
  .hero { padding: 62px 0 34px; }
  .hero--lead { padding: 52px 0 36px; }
  .hero h1 { max-width: none; }
  .band { padding: 60px 0; }
  .mid { padding: 0 20px; margin-top: 40px; }
  .mid-3d { grid-template-columns: 1fr; }
  .actions { gap: 14px; }
  .get { width: 100%; }
  .get-body { flex: 1 1 auto; padding: 13px 18px; }
  .get-txt b { font-size: 16px; }
  .get-stub { padding: 13px 16px; }
  .alt-list { gap: 8px; }
  .vcard-head { padding: 20px 20px 18px; }
  .vfiles { padding: 0 20px 20px; }
  .vcard-log .notes { padding: 4px 20px 22px; }
  /* One column again: below this the gutter costs more than the second
     column is worth, and the measure goes to about thirty characters. */
  .notes ul { columns: 1; }
  .drawer-in { padding: 20px 18px 22px; }
  .drawer-body { grid-template-columns: minmax(0, 1fr); }
}

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

/* ═══ REDUCED MOTION ═══════════════════════════════════════════════
   Everything above is decoration over a page that already works. Here it
   all stops: no ambient loops, no reveal, no scroll-driven geometry. The
   canvas paints one frame and site.js never asks for another.

   The one thing that must NOT be reset is a plate's own transform. It is
   not motion, it is where the plate stands — `transform: none` on those
   would stack all six flat on top of each other at the front of the
   stage, which is why the reveal was moved onto the layer above them. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .bloom, .grain { animation: none; }
  .js [data-reveal],
  .js [data-split="chars"] .ch-i,
  .js [data-split="words"] .w-i {
    opacity: 1 !important;
    transform: none !important;
    translate: none !important;
    scale: none !important;
    filter: none !important;
  }
  .js .mid .float { opacity: 1; translate: none; }
  .float-in { animation: none; }
  /* The focus pull is theatre, and the caption that comes with it is
     information. Here the theatre stands down and the information stays:
     every plate sharp, every caption written out, which is also what
     site.js decides for the text itself. */
  .float-plate { filter: none !important; }
  .float-cap { opacity: 1 !important; }
  .drawer-in { opacity: 1; translate: none; }
}

/* ── the fingerprints ───────────────────────────────────────────────────
   Two homes, because they answer at two different moments. `.drawer-sum`
   is the one that matters: it appears in the receipt panel just after the
   click, while the file is still sitting unopened in a downloads folder.
   `.sums` is the <details> in each version card — the copy that survives
   with scripts off, where the drawer does not exist at all.

   The hash itself is `user-select: all`, so one click takes all 64
   characters. That is the whole ergonomics of this feature: nobody is
   going to drag-select a hex string by hand, and a "copy" button would be
   script-only on a page whose whole discipline is working without one. */

.drawer-sum {
  margin: -8px 0 18px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.25);
  font-size: 12.5px;
  color: var(--dim);
}
.drawer-sum b {
  display: inline-block;
  margin-right: 8px;
  color: var(--dim-2);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.drawer-sum > code {
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--ink);
  word-break: break-all;
  user-select: all;
}
.drawer-sum i {
  display: block;
  margin-top: 7px;
  font-style: normal;
  font-size: 11.5px;
  color: var(--dim);
}
.drawer-sum i code {
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 11px;
  color: var(--dim-2);
}

.sums {
  margin: 14px 0 0;
  border-top: 1px solid var(--line);
  padding-top: 12px;
  font-size: 13px;
}
.sums > summary {
  cursor: pointer;
  list-style: none;
  color: var(--dim);
  transition: color 0.2s ease;
}
.sums > summary::-webkit-details-marker { display: none; }
.sums > summary::before {
  content: "+";
  display: inline-block;
  width: 14px;
  color: var(--tone);
  font-weight: 700;
}
.sums[open] > summary::before { content: "\2212"; }
.sums > summary:hover { color: var(--ink); }
.sums > summary span {
  margin-left: 8px;
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim-2);
  opacity: 0.7;
}

.sums-in { padding: 12px 0 4px 14px; }
.sums-how { margin: 0 0 10px; color: var(--dim); font-size: 12.5px; }
.sums-list { margin: 0 0 14px; }
.sums-list dt {
  font-size: 12px;
  color: var(--dim-2);
  margin-top: 8px;
}
.sums-list dd { margin: 2px 0 0; }
.sums-list code {
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 11.5px;
  color: var(--ink);
  word-break: break-all;
  user-select: all;
}
.sums-cmds { margin: 0; padding: 0; list-style: none; }
.sums-cmds li { margin-top: 5px; font-size: 12px; color: var(--dim); }
.sums-cmds b {
  display: inline-block;
  min-width: 62px;
  color: var(--dim-2);
  font-weight: 600;
}
.sums-cmds code {
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 11.5px;
  color: var(--dim-2);
}
