/* ==========================================================================
   HoldStage — landing page
   --------------------------------------------------------------------------
   The material is Apple's "liquid glass", which is also what the app's own
   shelf is made of — so the page is not decorated like the product, it is made
   of the same thing. Three parts do the work, and all three have to be present
   or it collapses into flat 2018 glassmorphism:

     1. blur() *and* saturate(). Saturation is what reads as refraction; blur
        on its own is grey fog.
     2. A directional specular rim. Real glass catches light on one side of its
        edge — a uniform 1px white border is the single loudest "AI glass" tell.
        Done here with a mask-composite gradient ring so it follows the radius.
     3. Grain. Mathematically smooth gradients are the other giveaway.

   backdrop-filter is the most expensive property in CSS, so it is rationed:
   `.glass` (real backdrop-filter) is used on exactly three surfaces — the
   sticky nav, the hero stage, the download slab. Everything else uses
   `.panel`, which is the same visual grammar minus the filter. They sit over
   the same aurora, so the difference is barely legible; the cost difference
   is enormous. Glass is also never nested inside glass — nested backdrop roots
   behave inconsistently across engines and double the work. The one place the
   product demands a second glass surface (the shelf, inside the stage) uses a
   plain translucent fill instead, exactly as the app's tiles do inside its
   own panel.
   ========================================================================== */

/* ---------------------------------------------------------------- tokens */
:root {
  --ink:       #f1f4ff;
  --ink-dim:   rgb(231 237 255 / .72);
  /* .58 rather than .40: this token carries real prose (the hero fineprint, the
     download notes), and .40 measures below WCAG AA's 4.5:1 on this background. */
  --ink-faint: rgb(231 237 255 / .58);

  --void:      #05060f;
  /* Lifted straight out of Tools/GenerateAppIcon.swift — the indigo field, the
     cyan bloom behind the slab and the violet second bloom. The page and the
     icon are the same three colours, so the download button and the thing it
     downloads look like one object. */
  --brand:     #4f67f2;
  --brand-up:  #85a2ff;
  --cyan:      #60f2ff;
  --violet:    #7873ff;

  /* Diagonal, so every panel has a lit side and a shadowed side. */
  --fill: linear-gradient(148deg,
            rgb(255 255 255 / .105) 0%,
            rgb(255 255 255 / .042) 42%,
            rgb(255 255 255 / .028) 60%,
            rgb(255 255 255 / .072) 100%);
  --frost: blur(30px) saturate(190%) brightness(1.06);

  /* Tight contact shadow + wide ambient one. */
  --lift: 0 2px 6px rgb(2 4 12 / .34), 0 28px 64px -18px rgb(2 4 12 / .72);

  --r-lg: 30px;
  --r-md: 20px;
  --r-sm: 12px;

  --spring: cubic-bezier(.32, 1.28, .38, 1);
  --glide:  cubic-bezier(.22, .78, .26, 1);

  /* The page is edge-to-edge (viewport-fit=cover), so on a notched iPhone in
     landscape the safe area is wider than the design gutter. max() takes whichever
     is larger, which keeps every section clear of the notch and the home indicator
     without touching the layout on any device that has neither. */
  --gutter-base: clamp(1.25rem, 5vw, 5rem);
  --gutter: max(var(--gutter-base), env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));
  --measure: 1200px;

  /* Body text is the system face: on a Mac that is SF Pro, which is exactly
     right for a Mac utility and costs nothing to load. The display face is
     Instrument Sans — tighter and more characterful than the usual grotesques,
     and it keeps the headlines from reading as stock UI. */
  --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Instrument Sans", system-ui, sans-serif;
  --display: "Instrument Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* ---------------------------------------------------------------- reset */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  min-height: 100vh;
  background: var(--void);
  color: var(--ink);
  font: 400 clamp(1rem, .96rem + .2vw, 1.06rem)/1.62 var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* `clip` rather than `hidden`: `overflow-x: hidden` makes the element a scroll
     container, which breaks `position: sticky` on the nav in several engines.
     `clip` cuts the overflow without that side effect; the `hidden` line stays as
     the fallback for engines that don't know `clip` yet. */
  overflow-x: hidden;
  overflow-x: clip;
}

img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; padding: 0; }
:focus-visible { outline: 2px solid var(--brand-up); outline-offset: 3px; border-radius: 6px; }

/* ---------------------------------------------------------------- atmosphere */
.atmosphere { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }

/* Enormous, slow, pre-blurred colour masses. Without them the glass blurs a flat
   black backdrop and the whole material dies. Only `transform` animates —
   animating `filter` or `background-position` would re-run the blur every frame. */
.aurora {
  position: absolute;
  border-radius: 50%;
  filter: blur(110px);
  opacity: .5;
  will-change: transform;
}
.aurora--a {
  width: 62vw; height: 62vw; min-width: 460px; min-height: 460px;
  top: -24vw; left: -14vw;
  background: radial-gradient(circle at 40% 40%, var(--brand), rgb(79 103 242 / 0) 68%);
  animation: drift-a 28s var(--glide) infinite alternate;
}
.aurora--b {
  width: 54vw; height: 54vw; min-width: 400px; min-height: 400px;
  top: 4vh; right: -18vw;
  background: radial-gradient(circle at 50% 50%, var(--violet), rgb(120 115 255 / 0) 66%);
  animation: drift-b 34s var(--glide) infinite alternate;
}
.aurora--c {
  width: 46vw; height: 46vw; min-width: 340px; min-height: 340px;
  top: 52vh; left: 24vw;
  opacity: .26;
  background: radial-gradient(circle at 50% 50%, var(--cyan), rgb(96 242 255 / 0) 64%);
  animation: drift-c 41s var(--glide) infinite alternate;
}
@keyframes drift-a { to { transform: translate3d(9vw, 7vh, 0) scale(1.14); } }
@keyframes drift-b { to { transform: translate3d(-11vw, 12vh, 0) scale(.88); } }
@keyframes drift-c { to { transform: translate3d(7vw, -11vh, 0) scale(1.2); } }

/* Faint structure for the glass to distort. Without something with edges behind
   it, a backdrop-filter has only soft colour to work on and reads as fog. */
.grid-field {
  position: absolute;
  inset: -1px;
  background-image:
    linear-gradient(rgb(255 255 255 / .045) 1px, transparent 1px),
    linear-gradient(90deg, rgb(255 255 255 / .045) 1px, transparent 1px);
  background-size: 88px 88px;
  mask-image: radial-gradient(ellipse 94% 70% at 50% 18%, #000 6%, transparent 76%);
}

/* Static, deliberately. Animated feTurbulence is a Safari performance disaster,
   and the still grain does the whole job: it kills gradient banding and takes
   the CG sheen off every smooth surface on the page. */
.grain {
  position: absolute;
  inset: 0;
  opacity: .14;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) { .aurora { animation: none; } }

@media (max-width: 40rem) {
  /* Phone GPUs pay for a large blur radius far more than a desktop one does, and the
     aurorae are the biggest blurred surfaces on the page — three of them, animating,
     beneath three backdrop-filtered panels. At this size the colour masses are only
     ~400px across, so a smaller radius is not visible, and it takes a real bite out
     of the per-frame compositing cost. */
  .aurora { filter: blur(64px); }
  /* Proportion, not performance: an 88px grid across a 375px screen is four cells,
     which reads as a few stray lines rather than as a grid. */
  .grid-field { background-size: 64px 64px; }
}

/* ---------------------------------------------------------------- material */
.glass, .panel {
  position: relative;
  isolation: isolate;
  border-radius: var(--r-lg);
  background: var(--fill);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .14),          /* bright top lip */
    inset 0 -14px 26px -14px rgb(0 0 0 / .38),     /* dark lower belly */
    var(--lift);
}

/* The only three surfaces that pay for a backdrop filter. */
.glass {
  -webkit-backdrop-filter: var(--frost);
          backdrop-filter: var(--frost);
}

/* The specular rim. A padding-box/border-box mask XOR leaves a 1px ring, which —
   unlike `border` — can carry a gradient, so light falls off around the shape the
   way it does on a real bevelled edge. Brightest at top-left, the implied source. */
.glass::before, .panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
              rgb(255 255 255 / .58) 0%,
              rgb(255 255 255 / .13) 26%,
              rgb(255 255 255 / .03) 48%,
              rgb(255 255 255 / .10) 78%,
              rgb(255 255 255 / .34) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}

/* Sheen pooled at the top, as if lit from above and in front. Kept very low
   contrast — this is the detail that tips into cheap plastic the moment it shouts. */
.glass::after, .panel::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  background: radial-gradient(120% 76% at 22% -14%, rgb(255 255 255 / .15), transparent 62%);
  pointer-events: none;
}

/* Content sits above both pseudo-elements. */
.glass > *, .panel > * { position: relative; z-index: 3; }

/* Cursor-tracked specular hotspot; --mx/--my are written by app.js on pointermove.
   True refraction would need backdrop-filter: url(#displacement), which is
   Chromium-only — no use for a Mac app's audience. This reads as glass and ships
   everywhere. The faint cyan/warm pair at opposite corners fakes the chromatic
   split you get at the edge of a real lens. */
.glass--live::after {
  background:
    radial-gradient(240px 240px at var(--mx, 50%) var(--my, 0%), rgb(255 255 255 / .13), transparent 68%),
    radial-gradient(90% 60% at 0% 0%, rgb(96 242 255 / .11), transparent 55%),
    radial-gradient(90% 60% at 100% 100%, rgb(255 120 160 / .09), transparent 55%),
    radial-gradient(120% 76% at 22% -14%, rgb(255 255 255 / .15), transparent 62%);
}

/* ---------------------------------------------------------------- type */
h1, h2, h3 {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -.035em;
  line-height: .99;
  text-wrap: balance;
}

/* Capped at 3.85rem rather than the 4.5rem a hero usually wants: the copy column is
   ~465px at every width above 1200px (--measure caps it), and "Free your hand." sets
   454px at this size. One notch larger and the second line breaks in half. */
h1 { font-size: clamp(2.6rem, 4.3vw, 3.85rem); }
h2 { font-size: clamp(2rem, 4.2vw, 3.3rem); }
h3 { font-size: 1.2rem; line-height: 1.24; letter-spacing: -.022em; }

/* Gradient text exactly once on the page — in the hero. Everywhere else the
   emphasis comes from contrast, which is why this one still lands. */
.hero h1 em {
  font-style: normal;
  background: linear-gradient(96deg, var(--brand-up) 0%, var(--cyan) 48%, var(--violet) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}
h3 em { font-style: italic; color: var(--brand-up); }

.eyebrow {
  display: block;
  margin-bottom: 1.05rem;
  font: 500 .72rem/1 var(--mono);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

kbd {
  display: inline-grid;
  place-items: center;
  min-width: 1.85em;
  height: 1.85em;
  padding: 0 .4em;
  border-radius: 7px;
  background: linear-gradient(180deg, rgb(255 255 255 / .17), rgb(255 255 255 / .055));
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .44),
    inset 0 0 0 1px rgb(255 255 255 / .1),
    0 2px 4px rgb(0 0 0 / .34);
  /* Deliberately the system face, not the mono: ⌃⌥⇧⌘ and the arrows come from
     SF Pro on a Mac, which draws them exactly as the OS does. JetBrains Mono
     substitutes a fallback for several of them and ⇧ lands as a hollow diamond. */
  font: 500 .82em/1 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

code { font: 400 .82em/1.5 var(--mono); color: var(--ink-dim); overflow-wrap: anywhere; }

/* ---------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  max-width: 100%;
  padding: .92rem 1.5rem;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: -.012em;
  white-space: nowrap;
  transition: transform .32s var(--spring), box-shadow .32s var(--glide), background-color .3s var(--glide);
}
/* Lift on hover only where hovering is a thing. On a touchscreen `:hover` sticks
   after a tap, so the button stays raised until you touch elsewhere. */
@media (hover: hover) {
  .btn:hover { transform: translateY(-2px); }
}
.btn:active { transform: translateY(0) scale(.985); transition-duration: .09s; }

.btn--primary {
  background: linear-gradient(178deg, var(--brand-up), var(--brand) 58%, #3346c9);
  color: #fff;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .46),
    inset 0 -1px 0 rgb(0 0 0 / .18),
    0 2px 8px rgb(30 46 140 / .4),
    0 16px 38px -10px rgb(79 103 242 / .62);
}
@media (hover: hover) {
  .btn--primary:hover {
    box-shadow:
      inset 0 1px 0 rgb(255 255 255 / .5),
      0 4px 12px rgb(30 46 140 / .44),
      0 24px 52px -12px rgb(133 162 255 / .78);
  }
}

.btn--ghost,
.btn--sm:not(.btn--primary) {
  background: rgb(255 255 255 / .065);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .14), inset 0 1px 0 rgb(255 255 255 / .22);
  color: var(--ink);
}
@media (hover: hover) {
  .btn--ghost:hover, .btn--sm:not(.btn--primary):hover { background: rgb(255 255 255 / .12); }
}

.btn--sm { padding: .55rem 1.1rem; font-size: .92rem; }
.btn--lg { padding: 1.1rem 2rem; font-size: 1.08rem; }

.btn__meta {
  padding-left: .72rem;
  border-left: 1px solid rgb(255 255 255 / .26);
  font: 400 .8em/1 var(--mono);
  opacity: .82;
}

/* "Download HoldStage 1.0.0 · 2.1 MB · .dmg" on one nowrap line is ~370px, which runs
   off the side of every phone. Stack the metadata under the label instead of letting
   it push the pill past the viewport — the divider rule becomes a rule above. */
@media (max-width: 30rem) {
  .btn--lg {
    flex-direction: column;
    gap: .5rem;
    padding: .95rem 1.4rem;
    font-size: 1rem;
    white-space: normal;
    text-align: center;
  }
  .btn--lg .btn__meta {
    padding: .5rem 0 0;
    border-left: 0;
    border-top: 1px solid rgb(255 255 255 / .26);
    align-self: stretch;
  }
}

/* ---------------------------------------------------------------- nav */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: clamp(1rem, 4vw, 2.5rem);
  padding: .8rem var(--gutter);
  transition: background-color .4s var(--glide), box-shadow .4s var(--glide);
}
/* Frosted only once it actually overlaps content — a permanently frosted bar
   over the hero reads as a seam across the page. */
.nav.is-stuck {
  background: rgb(5 6 15 / .55);
  -webkit-backdrop-filter: blur(22px) saturate(180%);
          backdrop-filter: blur(22px) saturate(180%);
  box-shadow: 0 1px 0 rgb(255 255 255 / .07), 0 12px 32px -18px rgb(0 0 0 / .9);
}

.nav__brand {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin-right: auto;
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.06rem;
  letter-spacing: -.025em;
}
.nav__brand img { border-radius: 7px; }

.nav__links { display: flex; gap: 1.7rem; font-size: .94rem; color: var(--ink-dim); }
.nav__links a { transition: color .25s; }
@media (hover: hover) { .nav__links a:hover { color: var(--ink); } }
@media (max-width: 860px) { .nav__links { display: none; } }
/* Nav Download is the one control above the fold on a phone; 33px tall is too small
   to hit reliably while scrolling one-handed. */
@media (pointer: coarse) { .nav .btn--sm { padding: .7rem 1.2rem; } }

/* ---------------------------------------------------------------- hero */
/* Asymmetric on purpose: copy left, product right. A centred hero column is the
   layout every generated landing page reaches for first. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr);
  align-items: center;
  gap: clamp(2.5rem, 5vw, 4.5rem);
  max-width: var(--measure);
  margin-inline: auto;
  padding: clamp(2.5rem, 6vw, 5rem) var(--gutter) clamp(2.5rem, 5vw, 4rem);
}
@media (max-width: 1000px) {
  .hero { grid-template-columns: 1fr; gap: 3rem; }
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  padding: .42rem 1rem .42rem .7rem;
  border-radius: 999px;
  background: rgb(255 255 255 / .06);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .13), inset 0 1px 0 rgb(255 255 255 / .2);
  font-size: .845rem;
  color: var(--ink-dim);
  transition: background-color .3s, color .3s;
}
@media (hover: hover) { .pill:hover { background: rgb(255 255 255 / .1); color: var(--ink); } }
.pill__dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: #37d67a;
  box-shadow: 0 0 0 3px rgb(55 214 122 / .2), 0 0 12px rgb(55 214 122 / .9);
}

.hero h1 { margin: 1.5rem 0 0; }

.lede {
  max-width: 42ch;
  margin-top: 1.4rem;
  font-size: clamp(1.04rem, .98rem + .35vw, 1.18rem);
  line-height: 1.58;
  color: var(--ink-dim);
  text-wrap: pretty;
}

.cta { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: 2rem; }
.fineprint { margin-top: 1.25rem; font-size: .83rem; color: var(--ink-faint); }

/* ==========================================================================
   The stage — the hero demo
   --------------------------------------------------------------------------
   The whole product is one gesture: you pick a file up, the shelf arrives, you
   put the file down, your hand is free, you go somewhere else, you pick it back
   up. Describing that takes a paragraph and proves nothing. So the hero is the
   gesture, playable: a real pointer drag, a real drop target, and — the part
   that is the actual point — the window behind CHANGES while the file is parked,
   because that is the thing you could not do with a file in your hand.
   ========================================================================== */
.screen { overflow: hidden; border-radius: var(--r-lg); }

.screen__bar {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .78rem 1.1rem;
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / .08);
}
.dot { width: 11px; height: 11px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgb(0 0 0 / .18); }
.dot--r { background: #ff5f57; } .dot--y { background: #febc2e; } .dot--g { background: #28c840; }

.screen__label {
  margin-inline: auto;
  padding-right: 44px;                  /* optically centre against the three dots */
  font: 500 .8rem/1 var(--mono);
  letter-spacing: .04em;
  color: var(--ink-faint);
  transition: color .3s var(--glide);
}
.screen__label.is-hot { color: var(--brand-up); }

/* The desktop: one app window filling it, and the shelf floating above at the
   right edge. Absolutely positioned rather than given a grid column of its own,
   because that is what the app does — an NSPanel over everything, which does not
   reflow your windows and leaves no empty lane behind when it is away. */
.desktop {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  aspect-ratio: 16 / 10.4;
  padding: 10px;
  background:
    radial-gradient(70% 90% at 16% 6%, rgb(120 115 255 / .34), transparent 62%),
    radial-gradient(80% 90% at 94% 88%, rgb(96 242 255 / .24), transparent 58%),
    linear-gradient(160deg, #10143a, #070a1a 62%, #0b0e26);
  /* The drag ghost is positioned against this box, and a tile dragged to the
     shelf lane must not paint outside it. */
  overflow: hidden;
  touch-action: pan-y;                  /* the page still scrolls; tiles opt out below */
}
@media (max-width: 40rem) {
  .desktop { padding: 7px; aspect-ratio: 16 / 13; }
}
/* A phone held sideways has ~390px of height and the aspect box wants far more at
   that width — the demo would not fit on screen in one piece. Drive it off the
   viewport height instead; nothing about the interaction changes. */
@media (orientation: landscape) and (max-height: 32rem) {
  .desktop { aspect-ratio: auto; height: 66vh; }
}

/* --- the app window: Downloads, then Mail */
.appwin {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  border-radius: 12px;
  background: linear-gradient(168deg, rgb(20 24 52 / .96), rgb(11 14 34 / .96));
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .16),
    inset 0 0 0 1px rgb(255 255 255 / .08),
    0 18px 40px -14px rgb(0 0 0 / .8);
  overflow: hidden;
}

.appwin__bar {
  display: flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem .65rem;
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / .08);
}
.appwin__bar i {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: rgb(255 255 255 / .22);
}
.appwin__title {
  margin-left: .35rem;
  font: 600 .7rem/1 var(--display);
  letter-spacing: -.005em;
  color: rgb(255 255 255 / .82);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Both app screens occupy the same box; only one is `is-on` at a time. Keeping
   them stacked rather than swapping innerHTML is what lets the switch cross-fade,
   and the cross-fade is the beat where the visitor sees that the file stayed put
   while the app underneath changed. */
.appwin__body { position: relative; flex: 1; min-height: 0; }
.appview {
  position: absolute;
  inset: 0;
  padding: .7rem;
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px);
  transition: opacity .42s var(--glide), transform .42s var(--glide), visibility .42s;
}
.appview.is-on { opacity: 1; visibility: visible; transform: none; }
@media (prefers-reduced-motion: reduce) { .appview { transition-duration: .01ms; } }

/* --- files inside the Downloads view */
/* Fixed tracks, not 1fr: a stretched track sets the tile's width from the container,
   and at three files across a wide stage that came out narrower than "contract.pdf",
   so every name ellipsised. Finder doesn't stretch its icons either. */
.files {
  display: grid;
  grid-template-columns: repeat(auto-fill, 84px);
  align-content: start;
  justify-content: start;
  gap: .55rem .2rem;
  height: 100%;
  overflow: hidden;
}
@media (max-width: 40rem) {
  .files { grid-template-columns: repeat(auto-fill, 68px); gap: .4rem .1rem; }
}

/* A file. Also the shelf's tiles — same object in both places, which is the
   honest thing to draw: the shelf holds your file, not a copy of its icon. */
.tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .3rem;
  padding: .45rem .3rem .35rem;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: grab;
  /* Without this the browser claims the gesture for scrolling and the drag
     never starts on a phone. */
  touch-action: none;
  -webkit-user-select: none;
          user-select: none;
  transition: background-color .2s, transform .34s var(--spring), opacity .28s;
}
@media (hover: hover) { .tile:hover { background: rgb(255 255 255 / .08); } }
.tile:active { cursor: grabbing; }
/* The original stays in place, dimmed, while the ghost is in flight — the same
   thing Finder does, and it makes the drop feel like a move rather than a copy. */
.tile.is-lifted { opacity: .28; }
.tile.is-gone { opacity: 0; transform: scale(.8); pointer-events: none; }

.tile__icon {
  position: relative;
  width: 34px; height: 42px;
  border-radius: 5px 5px 4px 4px;
  background: linear-gradient(160deg, #fbfcff, #d7dcee);
  box-shadow: 0 3px 8px -2px rgb(0 0 0 / .55), inset 0 -1px 0 rgb(0 0 0 / .1);
}
/* The folded corner. Two triangles: the page's own back, and the shadow it casts. */
.tile__icon::after {
  content: "";
  position: absolute;
  top: 0; right: 0;
  border-width: 0 0 11px 11px;
  border-style: solid;
  border-color: transparent transparent rgb(255 255 255 / .95) transparent;
  filter: drop-shadow(-1px 1px 1px rgb(0 0 0 / .22));
  transform: rotate(180deg);
  transform-origin: top right;
}
.tile__kind {
  position: absolute;
  left: 0; right: 0; bottom: 5px;
  font: 700 .46rem/1 var(--mono);
  letter-spacing: .06em;
  text-align: center;
  color: #4b5aa8;
}
/* Image files get a picture, not a document — the shelf's whole job is holding
   things you can recognise at a glance. */
.tile__icon--img { background: linear-gradient(150deg, #7fb0ff, #b98bff 55%, #ff9ec4); }
.tile__icon--img::after { display: none; }
.tile__icon--img .tile__kind { color: rgb(255 255 255 / .92); }

.tile__name {
  max-width: 100%;
  font: 500 .6rem/1.15 var(--sans);
  text-align: center;
  color: rgb(255 255 255 / .78);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 40rem) {
  .tile__icon { width: 28px; height: 35px; }
  .tile__name { font-size: .54rem; }
}

/* --- the Mail view */
.compose { display: flex; flex-direction: column; gap: .45rem; height: 100%; }
.compose__row {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding-bottom: .4rem;
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / .08);
  font: 400 .66rem/1 var(--mono);
  color: rgb(255 255 255 / .4);
}
.compose__row b { font: 500 .66rem/1 var(--mono); color: rgb(255 255 255 / .72); }

/* The drop well. It is a real drop target — the demo's second half is dragging
   the file off the shelf into here. */
.well {
  flex: 1;
  display: grid;
  place-content: center;
  gap: .4rem;
  justify-items: center;
  min-height: 0;
  padding: .6rem;
  border-radius: 9px;
  box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / .12);
  text-align: center;
  transition: box-shadow .25s, background-color .25s;
}
.well.is-hot {
  background: rgb(133 162 255 / .12);
  box-shadow: inset 0 0 0 1.5px rgb(133 162 255 / .7), 0 0 22px -4px rgb(133 162 255 / .5);
}
.well__hint { font: 400 .62rem/1.3 var(--sans); color: rgb(255 255 255 / .42); }

/* The attachment that lands at the end. */
.attach {
  display: none;
  align-items: center;
  gap: .4rem;
  padding: .35rem .6rem .35rem .4rem;
  border-radius: 999px;
  background: rgb(255 255 255 / .1);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .16);
  font: 500 .62rem/1 var(--sans);
  color: rgb(255 255 255 / .88);
  animation: pop .5s var(--spring);
}
.attach.is-on { display: inline-flex; }
.attach svg { color: #37d67a; flex-shrink: 0; }
@keyframes pop { from { opacity: 0; transform: scale(.7); } }
@media (prefers-reduced-motion: reduce) { .attach { animation: none; } }

/* --- the shelf itself */
/* Parked at the right edge, bottom-anchored — Preferences' own default is
   `.rightBottom`, so this is where the app actually puts it. */
/* The lane the shelf lives in: a fixed strip down the right-hand edge, and the thing the
   drop is actually tested against. Invisible and inert — hit-testing is done against
   rectangles in app.js, not with elementFromPoint, so it never has to intercept a pointer
   and never blocks the window underneath it. */
.shelf-lane {
  position: absolute;
  z-index: 4;
  top: 0; right: 0; bottom: 0;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  width: 118px;
  padding: 10px;
  pointer-events: none;
}
@media (max-width: 40rem) { .shelf-lane { width: 88px; padding: 7px; } }

.shelf {
  position: relative;
  /* The lane is inert so it never intercepts anything; `pointer-events` inherits, so the
     slab has to opt its own tiles back in — without this you can drop a file ON the shelf
     and then never pick it up again. */
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 6px;
  width: 92px;
  padding: 8px 6px;
  border-radius: 14px;
  /* Not `.glass`: this sits inside .screen, which is already a backdrop root, and
     nesting backdrop-filters is both unreliable and expensive. A translucent fill
     over the wallpaper is indistinguishable at this size — the same compromise the
     app makes for the tiles floating above its own slab. */
  background: linear-gradient(155deg, rgb(226 236 255 / .2), rgb(180 205 255 / .09));
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .5),
    inset 0 0 0 1px rgb(255 255 255 / .2),
    0 16px 34px -12px rgb(0 0 0 / .7);
  /* Off-stage until a drag starts. translate, not display: the slide *is* the
     feature — the shelf arriving is the moment the app announces itself. */
  transform: translateX(calc(100% + 14px));
  opacity: 0;
  transition: transform .46s var(--spring), opacity .3s var(--glide);
}
.desktop.is-shelf-out .shelf { transform: none; opacity: 1; }
@media (max-width: 40rem) {
  .shelf { width: 66px; }
}
.shelf-lane.is-hot .shelf {
  background: linear-gradient(155deg, rgb(133 162 255 / .34), rgb(96 242 255 / .16));
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .6),
    inset 0 0 0 1.5px rgb(133 162 255 / .8),
    0 0 26px -2px rgb(133 162 255 / .55);
}
@media (prefers-reduced-motion: reduce) {
  .shelf { transition-duration: .01ms; }
}

/* Empty slots, so the shelf reads as a shelf before anything is on it. */
.shelf__slot {
  flex: 0 0 auto;
  height: 46px;
  border-radius: 9px;
  box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / .13);
}
@media (max-width: 40rem) { .shelf__slot { height: 36px; } }
.shelf__slot .tile { width: 100%; height: 100%; justify-content: center; padding: 3px; }
.shelf__slot .tile .tile__icon { width: 24px; height: 30px; }
.shelf__slot .tile .tile__name { display: none; }
.shelf__slot.is-full { box-shadow: none; }

.shelf__grip {
  align-self: center;
  width: 22px; height: 3px;
  margin-bottom: 2px;
  border-radius: 2px;
  background: rgb(255 255 255 / .3);
}

/* --- the thing that follows the pointer */
/* Fixed rather than absolute: a drag can leave the stage, and a fixed ghost keeps
   tracking the pointer instead of being clipped by the stage's overflow. */
.ghost {
  position: fixed;
  z-index: 90;
  /* A fixed width rather than the source tile's: a tile lifted off the shelf is only ~66px
     wide and its name is hidden there, so cloning its width would squeeze the name the
     moment the ghost puts it back on show. */
  width: 84px;
  padding: .45rem .3rem .35rem;
  pointer-events: none;
  /* Beats `.tile`'s own transform transition, which would otherwise animate the tilt on
     every pointermove and leave the ghost trailing the cursor. */
  transition: none;
  transform: translate(-50%, -50%) rotate(-4deg);
  filter: drop-shadow(0 12px 20px rgb(0 0 0 / .6));
  opacity: 1;
}
@media (max-width: 40rem) { .ghost { width: 68px; } }
/* Cloned from a tile, so it starts life with `.tile`'s own display; `.ghost` without
   `.is-on` is never in the document, and this keeps the two states explicit. */
.ghost:not(.is-on) { display: none; }
/* The scripted demo moves the ghost by transition rather than by pointer. */
.ghost.is-scripted { transition: left .85s var(--glide), top .85s var(--glide); }
@media (prefers-reduced-motion: reduce) { .ghost.is-scripted { transition-duration: .01ms; } }

/* --- the caption that narrates the four beats */
.beat {
  position: absolute;
  left: 50%;
  bottom: 10px;
  transform: translateX(-50%);
  max-width: min(78%, 30ch);
  padding: .34rem .8rem;
  border-radius: 999px;
  background: rgb(6 8 22 / .74);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .14);
  font: 500 .68rem/1.3 var(--mono);
  letter-spacing: .01em;
  text-align: center;
  color: var(--ink-dim);
  opacity: 0;
  transition: opacity .3s var(--glide);
  pointer-events: none;
}
.beat.is-on { opacity: 1; }
@media (max-width: 40rem) { .beat { font-size: .6rem; bottom: 7px; } }

/* --- the bar under the stage */
.trybar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .9rem 1.4rem;
  margin-top: .85rem;
  padding: 1rem 1.25rem;
  border-radius: var(--r-md);
}
/* The instruction and Reset share the top line; the four steps take the row under
   it whole, because at this width they wrap onto two lines of their own and a Reset
   pushed along behind them ends up stranded on a third. */
.trybar__hint { flex: 1 1 auto; font-size: .89rem; color: var(--ink-dim); }
.trybar__hint strong { color: var(--ink); font-weight: 600; }

/* Mouse wording by default — including on a touch-capable laptop, where both
   inputs exist. Only a device whose *primary* pointer is a finger is told to drag
   with a finger. */
.trybar__hint-tap { display: none; }
@media (hover: none) and (pointer: coarse) {
  .trybar__hint-drag { display: none; }
  .trybar__hint-tap { display: inline; }
}

.steps { display: flex; flex: 1 0 100%; order: 3; flex-wrap: wrap; gap: .4rem; }
.step {
  display: inline-flex;
  align-items: center;
  gap: .42rem;
  padding: .38rem .8rem;
  border-radius: 999px;
  background: rgb(255 255 255 / .06);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .12), inset 0 1px 0 rgb(255 255 255 / .18);
  font: 500 .78rem/1.35 var(--sans);
  color: var(--ink-faint);
  transition: background-color .3s, color .3s, box-shadow .3s;
}
.step__n {
  display: grid;
  place-items: center;
  width: 1.25rem; height: 1.25rem;
  border-radius: 50%;
  background: rgb(255 255 255 / .1);
  font: 600 .66rem/1 var(--mono);
}
.step.is-on {
  background: linear-gradient(178deg, var(--brand-up), var(--brand));
  color: #fff;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .42), 0 6px 18px -6px rgb(79 103 242 / .8);
}
.step.is-on .step__n { background: rgb(255 255 255 / .26); }
.step.is-done { color: var(--ink-dim); }
.step.is-done .step__n { background: rgb(55 214 122 / .28); color: #8df0b4; }

.replay {
  order: 2;
  flex-shrink: 0;
  padding: .38rem .9rem;
  border: 0;
  border-radius: 999px;
  background: rgb(255 255 255 / .07);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .14);
  font: 500 .78rem/1.35 var(--sans);
  color: var(--ink-dim);
  cursor: pointer;
  transition: background-color .24s, color .24s;
}
@media (hover: hover) { .replay:hover { background: rgb(255 255 255 / .13); color: var(--ink); } }
/* 28px tall is under every touch-target guideline. */
@media (pointer: coarse) {
  .replay { min-height: 44px; padding-inline: 1.1rem; }
  .step { min-height: 38px; }
}

/* ---------------------------------------------------------------- sections */
.section {
  max-width: var(--measure);
  margin-inline: auto;
  padding: clamp(3.5rem, 7vw, 6rem) var(--gutter);
}
.section--tight { padding-block: clamp(1.75rem, 3.5vw, 2.75rem); }

.section__head { max-width: 48ch; margin-bottom: clamp(2.5rem, 5vw, 3.5rem); }
.section__head p { margin-top: 1.1rem; font-size: 1.04rem; color: var(--ink-dim); text-wrap: pretty; }

/* ---------------------------------------------------------------- menu cards */
/* Four groups of unequal length. Left to auto-fit they break 3 + 1, stranding a
   lone card on its own row. A six-column track lets the top row take two cards of
   three and the bottom row two of three — balanced at every width. */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 1rem; }
@media (min-width: 940px) { .cards { grid-template-columns: repeat(2, 1fr); } }

.card { padding: 1.35rem 1.45rem 1.45rem; border-radius: var(--r-md); }
.card__title {
  margin-bottom: 1rem;
  font: 500 .68rem/1 var(--mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.row {
  display: flex;
  align-items: center;
  gap: .8rem;
  padding: .48rem 0;
  font-size: .93rem;
}
.row + .row { box-shadow: inset 0 1px 0 rgb(255 255 255 / .07); }
.row__glyph { flex-shrink: 0; color: var(--brand-up); opacity: .9; display: flex; }
.row__name { margin-right: auto; }
.row__note { flex-shrink: 0; font: 400 .78rem/1.3 var(--sans); color: var(--ink-faint); text-align: right; }
.row__keys { display: flex; gap: .18rem; flex-shrink: 0; }

/* ---------------------------------------------------------------- bring-back band */
/* Full bleed, and the only section that breaks the measure — the feature that
   turns "a shelf" into "a shelf you can trust" should not sit in the same box as
   everything else. */
.band {
  position: relative;
  margin-block: clamp(1rem, 2.5vw, 2rem);
  padding: clamp(3rem, 6.5vw, 5rem) 0;
  background: linear-gradient(180deg, transparent, rgb(79 103 242 / .07) 30%, rgb(120 115 255 / .07) 70%, transparent);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .07), inset 0 -1px 0 rgb(255 255 255 / .07);
}
.band__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(2rem, 5vw, 4rem);
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
@media (max-width: 880px) { .band__inner { grid-template-columns: 1fr; } }
.band__copy p { margin-top: 1.1rem; max-width: 44ch; color: var(--ink-dim); text-wrap: pretty; }
.band__keys { display: flex; align-items: center; gap: .5rem; margin-top: 1.6rem; font-size: .88rem; color: var(--ink-faint); }

/* The mini demo: three tiles on a shelf, one dragged off into nowhere, then
   brought back. Scripted — there is nothing to play with here, the hero already
   gave the visitor the controls. */
.mini {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* Shallower than the hero stage, and the shelf inside is drawn larger: this box has one
     object in it, parked at the right edge, and at 16/10 the other two-thirds of it were
     empty desktop with nothing to say. */
  aspect-ratio: 16 / 8.4;
  padding: 14px;
  border-radius: var(--r-md);
  background:
    radial-gradient(70% 90% at 20% 10%, rgb(120 115 255 / .3), transparent 60%),
    linear-gradient(160deg, #0f1333, #070a1a);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .09), var(--lift);
  overflow: hidden;
}
.mini__shelf {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 88px;
  padding: 9px 8px;
  border-radius: 14px;
  background: linear-gradient(155deg, rgb(226 236 255 / .2), rgb(180 205 255 / .09));
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .5), inset 0 0 0 1px rgb(255 255 255 / .2);
}
.mini__slot {
  display: grid;
  place-items: center;
  height: 52px;
  border-radius: 10px;
  box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / .13);
}
.mini__doc {
  width: 31px; height: 38px;
  border-radius: 4px;
  background: linear-gradient(160deg, #fbfcff, #d7dcee);
  box-shadow: 0 3px 8px -2px rgb(0 0 0 / .55);
  transition: transform .55s var(--spring), opacity .4s var(--glide);
}
/* Flung off to the left and gone — an accidental drag-out. */
.mini__doc.is-gone { transform: translate(-190px, 26px) rotate(-16deg) scale(.8); opacity: 0; }
.mini__caption {
  position: absolute;
  left: 14px; bottom: 14px;
  font: 500 .74rem/1.3 var(--mono);
  letter-spacing: .04em;
  color: var(--ink-faint);
  transition: color .3s;
}
.mini__caption.is-hot { color: var(--brand-up); }
@media (prefers-reduced-motion: reduce) { .mini__doc { transition-duration: .01ms; } }

/* ---------------------------------------------------------------- bento */
/* Six equal cards, two clean rows. One card spanning two columns makes seven
   units in a three-column grid, which can never fill evenly — the sixth card ends
   up stranded alone on a third row. */
.bento { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(268px, 100%), 1fr)); gap: 1rem; }
@media (min-width: 920px) { .bento { grid-template-columns: repeat(3, 1fr); } }

.feat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .65rem;
  padding: 1.7rem;
  border-radius: var(--r-md);
  transition: transform .45s var(--glide), box-shadow .45s var(--glide);
}
@media (hover: hover) {
  .feat:hover {
    transform: translateY(-3px);
    box-shadow:
      inset 0 1px 0 rgb(255 255 255 / .18),
      inset 0 -14px 26px -14px rgb(0 0 0 / .38),
      0 2px 6px rgb(2 4 12 / .34), 0 40px 72px -22px rgb(2 4 12 / .85);
  }
}
.feat p { font-size: .95rem; color: var(--ink-dim); text-wrap: pretty; }
.feat p em { font-style: italic; color: var(--ink); }

.feat__icon {
  display: grid;
  place-items: center;
  width: 42px; height: 42px;
  margin-bottom: .3rem;
  border-radius: 12px;
  background: linear-gradient(160deg, rgb(255 255 255 / .19), rgb(255 255 255 / .05));
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .4), inset 0 0 0 1px rgb(255 255 255 / .1);
  color: var(--brand-up);
}

/* ---------------------------------------------------------------- cheat sheet */
.cheats { padding: clamp(1.75rem, 4vw, 2.5rem); border-radius: var(--r-md); }
.cheats__head { margin-bottom: 1.5rem; }
.cheats__list {
  display: grid;
  /* min() is the guard that makes auto-fit safe below the stated minimum: a bare
     minmax(330px, …) keeps a 330px track on a 320px phone and pushes the row off
     the side, because auto-fit only ever removes *columns*, never shrinks the floor. */
  grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr));
  gap: .1rem clamp(1.5rem, 4vw, 3.5rem);
}
/* Two columns, not three: three pushed each gesture and its meaning to opposite
   ends of the column, which is the one relationship this table exists to show. */
@media (min-width: 780px) { .cheats__list { grid-template-columns: repeat(2, 1fr); } }
.cheats__list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem .9rem;
  padding: .62rem 0;
  font-size: .93rem;
  color: var(--ink-dim);
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / .06);
}
/* The fixed column keeps the gestures aligned with each other; below that width
   there is no room for a column *and* its label, so the label wraps under. */
.cheats__keys { display: flex; align-items: center; gap: .2rem; min-width: 9.5rem; }
@media (max-width: 26rem) { .cheats__keys { min-width: 0; } }
.cheats__keys span { padding: 0 .18rem; color: var(--ink-faint); font-size: .82em; }
.cheats__keys kbd:last-child { min-width: auto; padding: 0 .5em; }
.cheats__keys em {
  font: 500 .78rem/1.85 var(--sans);
  font-style: normal;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------- trust */
.trust {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .55rem 1.6rem;
  max-width: var(--measure);
  margin: clamp(1.5rem, 4vw, 3rem) auto 0;
  padding-inline: var(--gutter);
  font: 500 .78rem/1 var(--mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.trust li { display: flex; align-items: center; gap: 1.6rem; }
.trust li:not(:last-child)::after {
  content: "";
  width: 3px; height: 3px;
  border-radius: 50%;
  background: currentColor;
  opacity: .5;
}
/* The dots separate items on a single line. Once the list wraps to four lines on a
   phone, the item that ends a line trails a dot pointing at nothing — so drop them
   and let the wider gap do the separating. */
@media (max-width: 40rem) {
  .trust { gap: .6rem 1.15rem; letter-spacing: .1em; }
  .trust li { gap: 0; }
  .trust li:not(:last-child)::after { display: none; }
}

/* ---------------------------------------------------------------- download */
.download { padding: clamp(2.5rem, 6vw, 4rem) clamp(1.5rem, 5vw, 3.5rem); text-align: center; }

.download__icon {
  width: clamp(84px, 12vw, 112px);
  margin: 0 auto 1.4rem;
  border-radius: 24px;
  filter: drop-shadow(0 20px 40px rgb(79 103 242 / .45));
}
.download h2 { font-size: clamp(1.85rem, 3.8vw, 2.7rem); }
.download__sub { margin: .85rem auto 1.9rem; color: var(--ink-dim); }
.download .btn { margin-inline: auto; }

.specs {
  display: grid;
  /* One column on a phone: three 1fr tracks on a 320px screen give each spec ~100px,
     which wraps "macOS 15.0 or later" onto three lines and still clips the third
     column's heading. Stacked, each spec reads as a row of a table. */
  grid-template-columns: 1fr;
  gap: 1px;
  margin-top: 2.4rem;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: rgb(255 255 255 / .08);      /* shows through the gap as hairlines */
  text-align: left;
}
/* Exactly three, never auto-fit: the three short specs fill one row and the hash
   spans the next. auto-fit picks four at this width and strands an empty cell. */
@media (min-width: 34rem) { .specs { grid-template-columns: repeat(3, 1fr); } }
/* A grid item's default min-width is `auto` — i.e. its content — so the unbroken
   64-character hash widens its track and drags the whole slab past the viewport.
   min-width: 0 lets the track shrink and the hash wrap instead. */
.specs > div { min-width: 0; padding: .85rem 1.05rem; background: rgb(10 12 26 / .5); }
.specs--full { grid-column: 1 / -1; }
.specs dt {
  font: 500 .66rem/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.specs dd { margin: .42rem 0 0; font-size: .92rem; }

.download__note {
  max-width: 58ch;
  margin: 1.9rem auto 0;
  font-size: .87rem;
  line-height: 1.6;
  color: var(--ink-faint);
}
.download__note strong { color: var(--ink-dim); font-weight: 600; }

/* ---------------------------------------------------------------- footer */
.footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 2rem;
  max-width: var(--measure);
  margin: clamp(3rem, 7vw, 5rem) auto 0;
  padding: 2.5rem var(--gutter) 4rem;
  font-size: .87rem;
  color: var(--ink-faint);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .07);
}
.footer__brand { display: flex; align-items: center; gap: .55rem; color: var(--ink-dim); font-weight: 600; }
.footer__brand img { border-radius: 6px; }
.footer nav { display: flex; flex-wrap: wrap; gap: 1.3rem; }
.footer nav a { transition: color .25s; }
@media (hover: hover) { .footer nav a:hover { color: var(--ink); } }
.footer > p { flex-basis: 100%; }

/* ---------------------------------------------------------------- reveal */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  animation: rise .9s var(--glide) forwards;
  animation-delay: var(--d, 0ms);
}
@keyframes rise { to { opacity: 1; transform: none; } }

.on-scroll {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .8s var(--glide), transform .8s var(--glide);
}
.on-scroll.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal { animation: none; opacity: 1; transform: none; }
  .on-scroll { opacity: 1; transform: none; transition: none; }
}

/* --------------------------------------------------------------------------
   Fallbacks
   -------------------------------------------------------------------------- */

/* Without backdrop-filter the glass surfaces would render as near-invisible
   4%-white rectangles. Give them an opaque tint instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass, .nav.is-stuck { background: rgb(21 25 48 / .94); }
}

/* Some people find translucency itself hard to read through, and macOS exposes
   that preference. Honour it: keep the layout, drop the see-through. */
@media (prefers-reduced-transparency: reduce) {
  .glass, .panel, .nav.is-stuck { background: #11152a; -webkit-backdrop-filter: none; backdrop-filter: none; }
  .glass::after, .panel::after, .glass--live::after { display: none; }
  .aurora { opacity: .3; }
  .grain { display: none; }
}
