/* ==========================================================================
   Faster - site stylesheet.

   The identity is monochrome: white ink on black, or black ink on #F4F4F2.
   Black is the home ground. The app is dark, and the one colour Faster owns
   is additive light: a spectrum band that hugs the window edge and the export
   progress bar. Additive light only exists against black, so this site is dark
   first and inverts whole under prefers-color-scheme: light.

   The band's four hues do NOT change with the theme. The app keeps one palette
   for every state and every appearance, and so does this. An earlier version of
   this file invented a darker, more saturated set for the light theme; that was
   a fabrication and it is gone.

   Loaded after Bootstrap 5.2.0, which is the only external dependency on the
   page and is used for its grid and a handful of utility classes. Everything
   with a look of its own is written here.
   ========================================================================== */

/* ── Fonts ───────────────────────────────────────────────
   Self hosted, licence files sit next to the files themselves:
   ZalandoSans-OFL.txt, ZalandoSansExpanded-OFL.txt, IBMPlexMono-OFL.txt.

   Zalando Sans for body copy and Zalando Sans Expanded for headings, the same
   pairing and the same files as the PlainText site, so the Metromedya sites
   read as one family. This was the owner's call on 2026-08-23: the brand book
   used to forbid setting anything in an expanded face on the grounds that it
   would approximate the SF Pro Expanded Black wordmark. The wordmark itself is
   still only ever a PNG, never typeset.

   IBM Plex Mono carries the numbers, because the app is full of them: 2x,
   30 fps, 1:00 to 0:30, roughly 5.6 MB. Figures that are compared should line
   up in a column.
   ─────────────────────────────────────────────────────── */

@font-face {
  font-family: 'Zalando Sans';
  src: url('../assets/fonts/ZalandoSans-VariableFont_wdth_wght.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-stretch: 75% 125%;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Zalando Sans';
  src: url('../assets/fonts/ZalandoSans-Italic-VariableFont_wdth_wght.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-stretch: 75% 125%;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Zalando Sans Expanded';
  src: url('../assets/fonts/ZalandoSansExpanded-VariableFont_wght.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Zalando Sans Expanded';
  src: url('../assets/fonts/ZalandoSansExpanded-Italic-VariableFont_wght.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'IBM Plex Mono';
  src: url('../assets/fonts/IBMPlexMono-300.woff2') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'IBM Plex Mono';
  src: url('../assets/fonts/IBMPlexMono-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'IBM Plex Mono';
  src: url('../assets/fonts/IBMPlexMono-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'IBM Plex Mono';
  src: url('../assets/fonts/IBMPlexMono-600.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* ── Tokens ────────────────────────────────────────────── */

:root {
  /* Ground and ink. Dark is the home ground. */
  --ground: #000000;
  --raised: #0B0B0D;
  --raised-2: #141417;
  --ink: #ffffff;
  --muted: rgba(255, 255, 255, 0.6);
  --faint: rgba(255, 255, 255, 0.38);
  --rule: rgba(255, 255, 255, 0.14);
  --rule-strong: rgba(255, 255, 255, 0.3);

  /* The amber accent belongs to the app interface. Here it is allowed on a
     link hover and on the primary button, and nowhere else. */
  --accent: #ffc24d;
  --accent-ink: #000000;
  /* The floating language pill sits over whatever scrolls under it. */
  --chip-bg: rgba(0, 0, 0, 0.72);

  /* The film strip in the style panel. Monochrome, like everything else: the
     object is suggested with a punched edge, a bevel and a shadow, never with
     colour. */
  /* One perforation tile: film, with a rounded rectangular hole cut out.
     Drawn as a single evenodd path so the inner rectangle is a hole rather
     than a shape sitting on top. */
  --perf: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='11'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h13v11H0z M4.4 3 h4.2 a1.3 1.3 0 0 1 1.3 1.3 v2.4 a1.3 1.3 0 0 1 -1.3 1.3 h-4.2 a1.3 1.3 0 0 1 -1.3 -1.3 v-2.4 a1.3 1.3 0 0 1 1.3 -1.3 z'/%3E%3C/svg%3E");
  --film: #1d1d21;
  --film-edge: #34343b;
  --film-divider: rgba(0, 0, 0, 0.85);
  --film-frame: #34343a;
  --film-hi: rgba(255, 255, 255, 0.1);

  /* The fps readout. A small recessed plate with the figure struck into it,
     the way a frame rate is marked on a camera body. Monochrome: the effect is
     carried by the lip of light along the bottom edge and the shadow the
     digits cast, never by colour. */
  --fps-plate: #101013;
  --fps-ink: rgba(255, 255, 255, 0.74);
  --fps-lip: rgba(255, 255, 255, 0.07);
  --fps-well: rgba(0, 0, 0, 0.6);
  --fps-carve-up: rgba(0, 0, 0, 0.85);
  --fps-carve-dn: rgba(255, 255, 255, 0.11);

  /* The spectrum. FSEdgeGlow.palette in FasterApp/DesignSystem.swift, whose
     own comment begins "One palette for every state": there is exactly one set
     of four and it is identical in every appearance, so there is no light
     variant of these and there must never be one. The second is coral rather
     than amber on purpose; the app tried amber and it came out as a
     yellow-white stretch of border once the bloom was added.

     The rim itself does not read these. js/spectrum.js carries the app's own
     float values so nothing is lost to hex rounding. These exist for the flat
     CSS uses: the timeline's fill. */
  --rim-1: #ff5c9e;
  --rim-2: #ff9457;
  --rim-3: #59e6d9;
  --rim-4: #8c6bff;

  --sans: 'Zalando Sans', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI',
    'Helvetica Neue', 'Noto Sans', 'Hiragino Sans', 'Yu Gothic', 'Apple SD Gothic Neo',
    'Malgun Gothic', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
  /* Headings use the expanded cut, as on the PlainText site. This was a
     deliberate call by the owner on 2026-08-23, overriding the brand book line
     that forbids approximating the SF Pro Expanded Black wordmark with a web
     font: the family reads as one family, and the wordmark stays a PNG. */
  --font-heading: 'Zalando Sans Expanded', 'Zalando Sans', -apple-system, BlinkMacSystemFont,
    'Helvetica Neue', 'Segoe UI', 'Hiragino Sans', 'Apple SD Gothic Neo', 'PingFang SC',
    Arial, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas,
    'Liberation Mono', monospace;

  /* One size for the app icon wherever it appears, the front page and the
     three sub pages alike. The family holds a flat 156px in both places. */
  --app-icon-size: clamp(7rem, 17vw, 9.75rem);

  --measure: 46rem;
  --gutter: clamp(1.25rem, 5vw, 2.5rem);
  --radius: 14px;
}

@media (prefers-color-scheme: light) {
  :root {
    --ground: #f4f4f2;
    --raised: #ffffff;
    --raised-2: #ffffff;
    --ink: #0b0b0c;
    --muted: rgba(11, 11, 12, 0.66);
    --faint: rgba(11, 11, 12, 0.44);
    --rule: rgba(11, 11, 12, 0.14);
    --rule-strong: rgba(11, 11, 12, 0.28);
    --accent: #a85400;
    --accent-ink: #ffffff;
    --chip-bg: rgba(244, 244, 242, 0.85);
    --film: #b8b8b0;
    --film-edge: #a5a59c;
    --film-divider: rgba(11, 11, 12, 0.4);
    --film-frame: #e9e7e1;
    --film-hi: rgba(255, 255, 255, 0.85);
    --fps-plate: #e2e0da;
    --fps-ink: rgba(11, 11, 12, 0.66);
    --fps-lip: rgba(255, 255, 255, 0.95);
    --fps-well: rgba(11, 11, 12, 0.22);
    --fps-carve-up: rgba(11, 11, 12, 0.3);
    --fps-carve-dn: rgba(255, 255, 255, 0.95);
    /* --rim-1..4 are deliberately NOT overridden here. See the note on them
       above: one palette for every appearance, as in the app. */
  }
}

/* ── Reset and base ────────────────────────────────────── */

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

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

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.62;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

/* No colour swap. The sibling sites all four hold the link's ink and answer
   the pointer with a rule under it; the amber that used to be here is not what
   the family does and not something the brand book asks for. The brand book
   leaves amber an open question and settles only that it is an interface
   accent which never enters the mark, and a link on a marketing page is
   neither.
   Where this goes past the family: the rule is there at rest as well, faint,
   and the link is set semibold. A link that is only a shade brighter than the
   sentence around it is not a link anyone can find, and brightness alone is
   not something every reader can see. The weight is a RESTING property on
   purpose. Bolding on hover would reflow the line under the pointer. */
a {
  color: var(--ink);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule-strong);
  text-underline-offset: 0.22em;
  transition: text-decoration-color 0.05s ease, text-decoration-thickness 0.05s ease;
}

/* Bootstrap styles a and a:hover, and it loads before this file, so every
   state has to be named here or its colour comes through. Dropping the colour
   from the hover rule was enough to hand hover back to Bootstrap and turn the
   links blue under the pointer. */
a,
a:link,
a:visited,
a:hover,
a:focus,
a:active {
  color: var(--ink);
}

/* focus-visible is ours. The family styles hover only, so a link reached with
   the keyboard shows nothing there. */
a:hover,
a:focus-visible {
  text-decoration-color: currentColor;
  text-decoration-thickness: 2px;
}

/* Links that are already a shape of their own: artwork, a bordered button, a
   menu row. A rule across the bottom of those is noise, and the family exempts
   its badges and icon links for the same reason. */
.app-store-badge,
a[aria-label],
a.icon-link,
a.btn,
a.skip-link,
a.footer-lockup,
a.publisher-link,
.language-selector a,
.language-menu a {
  font-weight: inherit;
  text-decoration: none;
}

/* The publisher's name in the colophon. A link, but not one the page is
   asking anyone to press, so it takes none of the hover treatment above and
   reads as the rest of the line. The pointer still turns into a hand and it
   still takes a focus ring; neither of those is hover. */
a.publisher-link {
  color: inherit;
}

a.publisher-link:hover {
  color: inherit;
  text-decoration: none;
}

@media (prefers-reduced-motion: reduce) {
  a {
    transition: none;
  }
}

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

hr.trail {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ink);
  color: var(--ground);
  padding: 0.75rem 1.25rem;
  z-index: 10000;
}

.skip-link:focus {
  left: 0;
  color: var(--ground);
}

/* ── Layout ────────────────────────────────────────────── */

.wrap {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: clamp(3rem, 8vw, 5.5rem);
}

.wrap--wide {
  max-width: 72rem;
}

.wrap--flush {
  padding-block: 0;
}

section > .wrap:first-child {
  padding-top: 0;
}

/* ── Type ──────────────────────────────────────────────── */

h1,
h2,
h3 {
  font-family: var(--font-heading);
  font-weight: 700;
  line-height: 1.14;
  letter-spacing: -0.02em;
  margin: 0;
  text-wrap: balance;
}

h1 {
  font-size: clamp(2rem, 5.2vw, 3.1rem);
}

h2 {
  font-size: clamp(1.55rem, 3.4vw, 2.2rem);
}

h3 {
  font-size: 1.125rem;
}

p {
  margin: 0 0 1.1em;
}

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

.eyebrow {
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 0.85rem;
}

.lede {
  font-size: 1.15rem;
  color: var(--muted);
  max-width: 40rem;
  margin-top: 1rem;
}

.note {
  font-size: 0.9rem;
  color: var(--faint);
  margin-top: 1.25rem;
}

.num {
  font-family: var(--mono);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* ── Language selector ─────────────────────────────────────
   Pinned to the top right, and it is the only thing above the hero. There is
   no masthead: none of the sibling sites has a <header> or a <nav> either,
   their documents open straight onto this control. Faster used to carry a
   lockup here too, which put the wordmark on screen twice before the visitor
   had scrolled anything.

   A <details> element, not a Bootstrap dropdown. It opens, closes and takes
   the keyboard with no script at all, which the dropdown cannot claim, and it
   carries no flag emoji, which the monochrome rule does not allow.
   js/site.js only adds Escape and outside-click closing.
   ─────────────────────────────────────────────────────── */

.language-selector {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
}

.language-selector summary {
  list-style: none;
  cursor: pointer;
  font-size: 0.9rem;
  color: var(--muted);
  padding: 0.5rem 0.95rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  white-space: nowrap;
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
  transition: border-color 0.18s ease, color 0.18s ease;
}

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

.language-selector summary:hover,
.language-selector[open] summary {
  color: var(--ink);
  border-color: var(--rule-strong);
}

.language-menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 50;
  margin: 0;
  padding: 0.4rem;
  list-style: none;
  min-width: 11rem;
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: 12px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
}

@media (prefers-color-scheme: light) {
  .language-menu {
    box-shadow: 0 18px 44px rgba(11, 11, 12, 0.14);
  }
}

.language-menu a {
  display: block;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  font-size: 0.92rem;
  color: var(--muted);
}

.language-menu a:hover,
.language-menu a:focus-visible {
  background: var(--raised-2);
  color: var(--ink);
  text-decoration: none;
}

.language-menu a[aria-current='true'] {
  color: var(--ink);
}

/* ── Buttons ───────────────────────────────────────────── */

/* The badge is the one thing on this screen a visitor is meant to press, and
   it was the most crowded thing on it: 28px above and 36px below, against the
   230px of nothing the scene below was sitting in. The room came from there. */
.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
  justify-content: center;
  margin-top: clamp(2.25rem, 4vw, 3.25rem);
}

.btn,
a.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--sans);
  font-size: 0.95rem;
  font-weight: 560;
  line-height: 1.2;
  padding: 0.8rem 1.4rem;
  border-radius: 999px;
  border: 1px solid var(--rule-strong);
  color: var(--ink);
  background: transparent;
  cursor: pointer;
  text-decoration: none;
  transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}

.btn:hover,
a.btn:hover,
.btn:focus-visible {
  border-color: var(--ink);
  color: var(--ink);
  text-decoration: none;
}

.btn--primary,
a.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.btn--primary:hover,
a.btn--primary:hover {
  color: var(--accent-ink);
  filter: brightness(1.08);
}

/* The Apple badge, same treatment and same rendered height as the sibling
   sites. The intrinsic width and height on the img are only there to reserve
   the box; the clamp below decides the real size. */
.app-store-badge {
  display: inline-block;
  transition: transform 0.2s ease;
}

.app-store-badge:hover {
  transform: scale(1.05);
}

/* Never recoloured, never inverted. Apple's marketing guidelines do not allow
   the badge artwork to be altered, and it does not need altering: the art is a
   black pill with a white stroke and white lettering, so it reads on this
   site's black ground and on the light one without any help. */
.app-store-badge img {
  height: clamp(56px, 8vw, 76px);
  width: auto;
}

/* No Mac App Store record exists yet, so the badge is shown but not linked.
   The line underneath carries what the old dashed placeholder used to say. */
.app-store-badge--pending {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
  cursor: default;
}

.app-store-badge--pending img {
  opacity: 0.55;
}

.app-store-badge-note {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
}

.pending {
  color: var(--faint);
  border-bottom: 1px dashed var(--rule-strong);
}

/* ── The spectrum rim ──────────────────────────────────────
   The app lights its window edge the way iOS lights the screen edge for Siri:
   a band of colour hugging the border, four hues drifting past each other at
   their own speeds so the combination never repeats. It does the same on the
   export progress bar.

   Here the band is a conic gradient on a pseudo element, oversized so that a
   rotation still covers the corners, sitting behind an opaque inner panel.
   What stays visible is the padding: a two pixel rim that turns.

   Faster's screenshots already carry the app's own rim, baked in. So the CSS
   rim is set well outside the screenshot rather than on top of it. You see
   the page's band, a gap, then the window's band. An echo, not a double.
   ─────────────────────────────────────────────────────── */

/* The export bar. FSGlowProgress: a capsule track at 8 percent white with the
   very same rim laid over the filled part, at full intensity, period 1.5 and a
   bloom scaled to 0.4 so four stacked layers on a solid shape do not burn to
   white. The geometry is the app's, multiplied by --bar-scale. */
.rim-progress {
  /* The app draws this 8 points tall. A web page is wider than the app's
     window, so the whole shape is scaled up a little and every number below
     rides that. The ceiling is 1.3: the band is a fixed 6.2 points, so past
     that it stops reaching the centre line and the bar turns into a hollow
     outline with a dark seam down the middle instead of a lit core. */
  --bar-scale: 1.25;
  --bar-height: calc(8px * var(--bar-scale));
  position: relative;
  height: var(--bar-height);
}

.rim-progress-track {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
}

@media (prefers-color-scheme: light) {
  .rim-progress-track {
    background: rgba(11, 11, 12, 0.08);
  }
}

/* Oversized by the app's 18 point margin on every side and pulled back by the
   same amount, so the bloom has room inside the surface instead of being
   clipped at the bounds. js/spectrum.js sizes it. */
.rim-progress-glow {
  position: absolute;
  left: calc(-18px * var(--bar-scale));
  top: calc(-18px * var(--bar-scale));
  pointer-events: none;
}

/* ── Hero ──────────────────────────────────────────────── */

.hero {
  text-align: center;
  /* The masthead used to hold this space open. Without it the hero has to
     clear the floating language pill on its own. */
  padding-top: clamp(4.5rem, 9vw, 7rem);
}

/* The hero used to carry the vertical lockup, mark above wordmark. It now
   leads with the square app icon, the way the four sibling sites do, and the
   name follows underneath. The lockup would have put the mark on the page
   twice, since the icon is the mark in a squircle.
   The family sets the name in type; Faster's wordmark is SF Pro Expanded
   Black on an eight degree slant and cannot be typed, so it stays an image.
   Only the ICON is new. The wordmark, the mark and the two lockups have not
   changed and are still the files copied from content/brand. */
.hero-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.9rem, 2.4vw, 1.4rem);
  margin: 0 auto clamp(2.75rem, 6vw, 4.5rem);
}

/* The icon and the rim surface over it. The corners are transparent in the
   file, so the shadow follows the squircle and no radius is set here.
   The family answers a hover on this with a scale and a five degree tilt.
   Faster answers it with the band instead, the same one the screenshot tiles
   get, so the icon behaves like the app's own window. A tilt underneath a
   drifting band would only drag the band around. */
.app-icon {
  position: relative;
  display: block;
  width: var(--app-icon-size);
  line-height: 0;
}

.app-icon img {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.55));
}

.app-icon canvas {
  position: absolute;
  pointer-events: none;
}

.hero-wordmark {
  display: block;
  width: min(100%, 13.5rem);
}

.hero-wordmark img {
  width: 100%;
}

/* Only ever seen by someone who tried to drop a file on the page. */
.drop-guard {
  max-width: 34rem;
  margin: 0.85rem auto 0;
  font-size: 0.9rem;
  color: var(--ink);
}

.drop-guard[hidden] {
  display: none;
}

.hero-meta {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--faint);
  margin-top: 1rem;
  letter-spacing: 0.02em;
}

.hero-copy {
  max-width: 38rem;
  margin: clamp(2.75rem, 4.5vw, 3.75rem) auto 0;
  color: var(--muted);
  text-align: left;
}

/* The scene sits in the "how it works" section, acting out step one, not in
   the hero. It was the hero's picture for a while and it was in the wrong
   place: it explains a drag and a drop, which is what the first step says. */
.drop-stage {
  max-width: 60rem;
  margin: clamp(1rem, 2.5vw, 1.75rem) auto clamp(2rem, 5vw, 3.5rem);
}

/* ── The hero scene ────────────────────────────────────────
   The walkthrough's second page, running on the page. Every number below is
   in the app's own stage units (a 300 x 196 stage holding a 200 x 112 window)
   multiplied by --scene-scale, which js/spectrum.js sets from the available
   width. Keeping the units means the cursor's path and the window's
   proportions are identical at every size, which is exactly why the app fixed
   the stage in the first place.
   ─────────────────────────────────────────────────────── */

/* The stage is 340 x 240 rather than the app's 300 x 196, and that difference
   is the one deliberate departure from its geometry.

   The window swells by eight percent at the drop. The glow surface swells with
   it, from 280 x 192 to 302 x 207, and the window's own shadow reaches about
   110 units below the centre. In a 196 tall stage with overflow hidden, both
   run straight into the clip and you see a hard straight edge cut across the
   glow. The app gets away with 196 because at its size the bloom has faded to
   nothing before it reaches the edge; scaled up on a page it has not.

   The extra room is padding only. The window stays 200 x 112 centred and the
   cursor path is still measured against the original 300 x 196 field, offset
   by --pad-x and --pad-y, so the motion is unchanged.
   ─────────────────────────────────────────────────────── */

.drop-scene {
  --scene-scale: 1;
  --u: calc(1px * var(--scene-scale));
  --pad-x: 20;
  --pad-y: 22;
  position: relative;
  width: calc(340 * var(--u));
  height: calc(240 * var(--u));
  margin-inline: auto;
  /* The stage is 240 units tall and the window is 112, so 64 units of empty
     stage sit above the window and 64 below. Above the window that space is
     dead: the bloom reaches 40 units and the swell four more, so the top 20 or
     so are never drawn into, and they were reading as a hole between the copy
     and the scene. This takes them out of the LAYOUT only. The stage still
     draws its full height, so the bloom is untouched; it simply overlaps the
     empty margin above it now instead of pushing it further away.
     Only the top. The bottom is where the cursor enters, at unit 237 of 240,
     and pulling the caption up into that would put it under the pointer.
     The element above is a heading now rather than a paragraph, so there is
     more clear space to take back than there was in the hero. */
  margin-top: calc(-22 * var(--u));
  /* Nothing in here is interactive, and that negative margin puts the stage's
     empty top over the last line of the copy above. Without this it would eat
     a selection drag that started on that line. */
  pointer-events: none;
  /* Still clipped: the cursor enters and leaves from outside the field and
     must not be seen doing it. */
  overflow: hidden;
}

.scene-window {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(200 * var(--u));
  height: calc(112 * var(--u));
  margin-left: calc(-100 * var(--u));
  margin-top: calc(-56 * var(--u));
  will-change: transform;
}

/* Oversized and inset by 40, so the bloom that spills outside the window has
   room instead of being clipped at the bounds. */
.scene-rim {
  position: absolute;
  left: calc(-40 * var(--u));
  top: calc(-40 * var(--u));
  width: calc(280 * var(--u));
  height: calc(192 * var(--u));
  pointer-events: none;
}

/* The shadow lives here rather than on .scene-window, because a filter on the
   window would apply to the whole subtree and put a shadow under the glow
   canvas as well. In the app the shadow is taken on the window before the glow
   is laid over it, for exactly that reason. */
.scene-face {
  position: absolute;
  inset: 0;
  border-radius: calc(9 * var(--u));
  background: #17171a;
  box-shadow: 0 calc(8 * var(--u)) calc(14 * var(--u)) rgba(0, 0, 0, 0.5);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(8 * var(--u));
  /* Half of this moves the centred group down clear of the window buttons,
     the way the real window centres its content under the title bar. */
  padding-top: calc(16 * var(--u));
}

@media (prefers-color-scheme: light) {
  .scene-face {
    background: #17171a;
  }
}

/* Window buttons, at the mini window's own scale: the real drop window is
   411pt across with 12pt buttons, this one is 200, so they come out at 6.
   Grey, which is how macOS draws them on a window that is not frontmost. */
.scene-chrome {
  position: absolute;
  top: calc(8 * var(--u));
  left: calc(9 * var(--u));
  display: flex;
  gap: calc(5 * var(--u));
  z-index: 1;
}

.scene-chrome span {
  width: calc(6 * var(--u));
  height: calc(6 * var(--u));
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
}

/* FSDotPattern: spacing 12, dot 1.5. */
.scene-dots {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(
    circle at center,
    rgba(255, 255, 255, 0.07) calc(0.75 * var(--u)),
    transparent calc(0.75 * var(--u))
  );
  background-size: calc(12 * var(--u)) calc(12 * var(--u));
}

/* FSBrandMark at 70 x 18: the ring, its filled centre, three blurred ghosts
   trailing behind it, and the streak they smear across the frame. --trail is
   0.78 at rest and 1 at the full swell, set by js/spectrum.js. */
.scene-mark {
  --trail: 0.78;
  position: relative;
  width: calc(70 * var(--u));
  height: calc(18 * var(--u));
  z-index: 1;
}

.mark-streak {
  position: absolute;
  left: 0;
  top: 50%;
  width: 100%;
  height: calc(8.24 * var(--u));
  margin-top: calc(-4.12 * var(--u));
  border-radius: 999px;
  filter: blur(calc(2.53 * var(--u)));
  background: linear-gradient(
    to right,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.05) 16%,
    rgba(255, 255, 255, 0.16) 38%,
    rgba(255, 255, 255, 0.07) 70%,
    rgba(255, 255, 255, 0) 100%
  );
}

.mark-ring,
.mark-ghost {
  position: absolute;
  top: 50%;
  width: calc(15.84 * var(--u));
  height: calc(15.84 * var(--u));
  margin-top: calc(-7.92 * var(--u));
  margin-left: calc(-7.92 * var(--u));
  border-radius: 50%;
  border: calc(2.376 * var(--u)) solid #fff;
  box-sizing: border-box;
}

/* Further out means blurrier and fainter: opacity 0.62/step, blur 0.055 x
   diameter x step, offset 0.70 x diameter x step x trail from the ring. */
.mark-ghost {
  left: calc((21 + 11.088 * var(--step) * var(--trail)) * var(--u));
  opacity: calc(0.62 / var(--step));
  filter: blur(calc(0.871 * var(--step) * var(--u)));
}

.mark-ring {
  left: calc(21 * var(--u));
}

.mark-ring::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(4.75 * var(--u));
  height: calc(4.75 * var(--u));
  margin: calc(-2.375 * var(--u)) 0 0 calc(-2.375 * var(--u));
  border-radius: 50%;
  background: #fff;
}

/* The cursor and the clip it carries. The clip rides a little below and right
   of the arrow tip, the way a dragged Finder item does. */
.scene-cursor {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(48 * var(--u));
  height: calc(38 * var(--u));
  margin-left: calc(-24 * var(--u));
  margin-top: calc(-19 * var(--u));
  pointer-events: none;
  z-index: 2;
  will-change: transform, opacity;
  /* Where the loop starts: parked off the field at (1.12, 1.10) and invisible.
     300 x 1.12 + 20 padding, 196 x 1.10 + 22 padding. Written here as well as
     in the script so that if the script never runs the cursor is not left
     sitting in the top left corner. */
  transform: translate(calc(356 * var(--u)), calc(237.6 * var(--u)));
  opacity: 0;
}

.scene-arrow {
  position: absolute;
  left: calc(24 * var(--u));
  top: calc(19 * var(--u));
  width: calc(12 * var(--u));
  height: calc(19 * var(--u));
  filter: drop-shadow(0 calc(1 * var(--u)) calc(1.5 * var(--u)) rgba(0, 0, 0, 0.7));
}

.scene-clip {
  position: absolute;
  left: calc(34 * var(--u));
  top: calc(31 * var(--u));
  width: calc(38 * var(--u));
  height: calc(26 * var(--u));
  border-radius: calc(5 * var(--u));
  background: linear-gradient(135deg, #8c8c8c, #474747);
  box-shadow:
    inset 0 0 0 calc(0.5 * var(--u)) rgba(255, 255, 255, 0.25),
    0 calc(3 * var(--u)) calc(6 * var(--u)) rgba(0, 0, 0, 0.5);
}

/* The play glyph on the carried clip. */
.scene-clip::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  margin: calc(-5 * var(--u)) 0 0 calc(-3.5 * var(--u));
  border-style: solid;
  border-width: calc(5 * var(--u)) 0 calc(5 * var(--u)) calc(8 * var(--u));
  border-color: transparent transparent transparent rgba(255, 255, 255, 0.85);
}

.drop-stage figcaption {
  margin-top: 1rem;
  font-size: 0.85rem;
  color: var(--faint);
  text-align: center;
}

/* ── Gallery strip ─────────────────────────────────────── */

.gallery-section {
  position: relative;
}

/* The blurred edges. Same construction as every sibling site: two fixed width
   bands pinned to the left and right of the strip, blurring whatever passes
   behind them, faded out with a mask so there is no hard seam. They bleed past
   the strip vertically on purpose, which is why the numbers are negative. */
.gallery-scroll-wrapper {
  position: relative;
}

.gallery-scroll-wrapper::before,
.gallery-scroll-wrapper::after {
  content: '';
  position: absolute;
  top: -120px;
  bottom: -120px;
  width: 80px;
  z-index: 2;
  pointer-events: none;
  -webkit-backdrop-filter: blur(40px);
  backdrop-filter: blur(40px);
}

.gallery-scroll-wrapper::before {
  left: 0;
  -webkit-mask-image: linear-gradient(to right, black, transparent);
  mask-image: linear-gradient(to right, black, transparent);
}

.gallery-scroll-wrapper::after {
  right: 0;
  -webkit-mask-image: linear-gradient(to left, black, transparent);
  mask-image: linear-gradient(to left, black, transparent);
}

.scroll-container {
  overflow-x: scroll;
  overflow-y: hidden;
  white-space: nowrap;
  /* Room for the band's bloom. overflow-y: hidden clips at the padding box,
     and the band now sits on the edge of the tile rather than on a window
     inset well inside it, so without this the halo is sheared off top and
     bottom. Forty catches all of it that can be seen; the last stretch of the
     widest blur is under two percent alpha. */
  padding-top: 40px;
  padding-bottom: 52px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  touch-action: pan-x;
  -webkit-overflow-scrolling: touch;
}

.scroll-container::-webkit-scrollbar {
  display: none;
}

/* Full bleed strip whose first tile still lines up with the content column.
   72rem is this site's wide measure, where the sibling sites use their 1280px.
   Below that width the padding falls back to the gutter and the strip simply
   runs edge to edge. */
.gridscroll {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  padding-left: max(var(--gutter), calc((100vw - 72rem) / 2 + var(--gutter)));
  padding-right: max(var(--gutter), calc((100vw - 72rem) / 2 + var(--gutter)));
}

/* Faster used to skip the frame the sibling sites put here, because its
   screenshots were transparent PNGs carrying their own window corner and
   shadow. They are composed packshots now, opaque rectangles like the
   family's, so the frame comes back: a corner for the band to run around and
   a hairline so a dark packshot has an edge on a dark page. */
.gridscroll .shot {
  position: relative;
  flex: 0 0 auto;
  height: clamp(260px, 34vw, 440px);
}

.gridscroll picture {
  display: block;
  height: 100%;
}

/* The spectrum band, lit while the pointer is on the tile. This is where the
   band went after it was taken off the browser viewport: it means what it
   means in the app, which is that this is being pointed at. On a transparent
   screenshot js/spectrum.js puts it on the window measured out of the PNG; on
   a packshot there is no window to hug, so it runs around the tile. Either
   way a cold tile draws nothing at all. */
.shot-rim {
  position: absolute;
  pointer-events: none;
  z-index: 1;
}

.gridscroll img {
  height: 100%;
  width: auto;
  max-width: none;
  display: block;
  object-fit: contain;
  border-radius: 14px;
  /* Outset, so it rings the tile instead of covering its first pixel row.
     js/spectrum.js reads this radius off the element, so changing it here
     moves the band with it. */
  box-shadow: 0 0 0 1px var(--rule);
  transition: transform 0.24s ease;
}

/* No scale on hover any more. It predates the spectrum band and the two do not
   agree: the band is placed from the tile's layout box, so a transform on the
   image slides the window out from under it by a couple of pixels on every
   side. The band is the hover answer now. */

/* js/gallery.js turns each thumbnail into a real button, so it needs a focus
   ring. The offset keeps the ring clear of the shadow baked into the PNG. */
.gridscroll img:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -10px;
}

.custom-scrollbar-wrap {
  display: flex;
  align-items: center;
  height: 18px;
  max-width: 72rem;
  margin: 0.5rem auto 0;
  padding: 0 var(--gutter);
  cursor: pointer;
}

.custom-scrollbar-track {
  position: relative;
  width: 100%;
  height: 6px;
  border-radius: 10px;
  background: var(--rule);
  transition: height 0.3s ease;
}

.gallery-section:hover .custom-scrollbar-track {
  height: 12px;
}

.custom-scrollbar-thumb {
  position: absolute;
  top: 0;
  bottom: 0;
  min-width: 40px;
  border-radius: 10px;
  background: var(--rule-strong);
  cursor: grab;
  transition: background-color 0.3s ease;
}

.custom-scrollbar-thumb:hover {
  background: var(--ink);
}

.custom-scrollbar-thumb.dragging {
  background: var(--ink);
  cursor: grabbing;
}

/* ── Feature cards ─────────────────────────────────────── */

/* Separate bordered cards rather than one grid with hairline gaps. The gap
   trick needs the grid to be exactly full, and eight cards in a three column
   layout leave a ninth cell that reads as an empty panel. This survives any
   card count and any column count. */
.features-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1rem;
  margin-top: 2.5rem;
}

.feature-card {
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.5rem 1.4rem;
}

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

.feature-card p {
  color: var(--muted);
  font-size: 0.95rem;
  margin: 0;
}

/* ── Two controls: the speed and style matrix ──────────────
   This is Faster's own section, the equivalent of the log viewer on the
   MultiSim site. The app has exactly two controls that decide the result, so
   the page shows what each one does to one minute of video rather than
   describing it.
   ─────────────────────────────────────────────────────── */

.controls-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: clamp(1.25rem, 3vw, 2rem);
  margin-top: 2.5rem;
}

.control-panel {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: clamp(1.25rem, 3vw, 1.75rem);
  background: var(--raised);
}

.control-panel h3 {
  margin-bottom: 0.35rem;
}

.control-panel > p {
  color: var(--muted);
  font-size: 0.95rem;
}

.speed-list {
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
  display: grid;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 10px;
  overflow: hidden;
}

.speed-list li {
  display: grid;
  grid-template-columns: 3.5rem 1fr auto;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.6rem 0.85rem;
  background: var(--raised);
  font-size: 0.92rem;
}

.speed-list .speed-mult {
  font-family: var(--mono);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.speed-list .speed-out {
  font-family: var(--mono);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.speed-list .speed-note {
  color: var(--faint);
  font-size: 0.82rem;
  text-align: right;
}

.style-list {
  position: relative;
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.75rem;
}

/* One canvas for all three rows: js/spectrum.js moves it onto whichever fps
   badge is under the pointer. */
.badge-rim {
  position: absolute;
  pointer-events: none;
  z-index: 2;
}

.style-list li {
  /* The fps badge's rim canvas is absolutely placed against THIS box, so it
     has to be the positioned ancestor. Without it the canvas resolved against
     .style-list and every row after the first was drawn 152 and 303 pixels
     too high, stacked on top of the first badge. */
  position: relative;
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: 0.85rem 1rem;
  background: var(--ground);
}

.style-name {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-weight: 600;
}

/* Struck into a small plate rather than set as loose type. The figure sits in
   a shallow well: a shadow across the top inside edge, a lip of light along
   the bottom, and the digits themselves carved with a dark edge above and a
   pale one below. Tabular figures so 8 and 30 occupy the same plate. */
.style-fps {
  flex: 0 0 auto;
  /* One plate, three figures: 8 fps is a character shorter than 30 fps, and a
     badge that changes width with its contents does not read as a struck
     plate. Tabular figures plus a floor on the width keeps all three the
     same. Every language writes this as "N fps", so the floor holds. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 3.9rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--fps-ink);
  padding: 0.22em 0.6em 0.26em;
  border-radius: 5px;
  background: var(--fps-plate);
  box-shadow:
    inset 0 1.5px 2px var(--fps-well),
    inset 0 -1px 0 var(--fps-lip),
    0 1px 0 var(--fps-lip);
  text-shadow:
    0 -1px 0 var(--fps-carve-up),
    0 1px 0 var(--fps-carve-dn);
}

.style-list p {
  margin: 0.3rem 0 0;
  font-size: 0.88rem;
  color: var(--muted);
}

/* ── The film strip ────────────────────────────────────────
   One second of film per style, cut into as many frames as that style writes
   in a second: thirty, fifteen, eight. The count is not decoration, it is the
   thing the panel is describing, so a glance down the three rows shows a fine
   comb, then a coarser one, then a row of blocks.

   The perforations are rounded rectangles punched with an SVG mask, not dots
   or dashes from a gradient. That matters more than anything else here: a
   35mm perforation is wider than it is tall with a small radius, and the film
   between two holes is wider than the hole. An earlier version used square
   gaps as wide as the film between them and read as a dashed line rather than
   as an edge that had been punched.

   Monochrome, as everything else is. What makes it read as film is the
   perforation shape, the tonal step between the base and the frames, and the
   bevel, never colour. Nothing animates.
   ─────────────────────────────────────────────────────── */

.filmstrip {
  --sprocket: 11px;
  position: relative;
  height: 3.25rem;
  margin-top: 0.85rem;
  border-radius: 2px;
  overflow: hidden;
  background:
    /* a faint sheen, the way light sits along a strip of film */
    linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.07) 0%,
      rgba(255, 255, 255, 0) 45%,
      rgba(0, 0, 0, 0.1) 100%
    ),
    /* the frame line, one per frame */
    repeating-linear-gradient(
      90deg,
      transparent 0,
      transparent calc(100% / var(--frames) - 1px),
      var(--film-divider) calc(100% / var(--frames) - 1px),
      var(--film-divider) calc(100% / var(--frames))
    ),
    var(--film-frame);
  box-shadow:
    inset 0 0 0 1px var(--film-edge),
    inset 0 1px 0 var(--film-hi),
    0 1px 2px rgba(0, 0, 0, 0.35);
}

/* The two perforation rows. Solid film with the holes cut clean through to
   whatever is behind the strip, which is also what keeps the frame lines from
   showing in the margins. */
.filmstrip::before,
.filmstrip::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: var(--sprocket);
  background-color: var(--film);
  -webkit-mask-image: var(--perf);
  mask-image: var(--perf);
  -webkit-mask-size: 13px var(--sprocket);
  mask-size: 13px var(--sprocket);
  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
}

.filmstrip::before {
  top: 0;
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.45);
}

.filmstrip::after {
  bottom: 0;
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.45);
}

@media (max-width: 480px) {
  .filmstrip {
    height: 2.6rem;
    --sprocket: 9px;
  }
}

/* ── Export section ────────────────────────────────────── */

.export-panel {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--raised);
  padding: clamp(1.25rem, 3vw, 2rem);
  margin-top: 2.5rem;
}

/* The app gives this row a fixed height of 32 and swaps the progress bar for
   the receipt inside it. Both live here at once and cross fade, and the
   receipt is taken out of the flow, so neither swap moves anything: a change
   of width here would resize the bar, and resizing the bar rebuilds four
   render targets. */
.export-bar-row {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 32px;
  margin-bottom: 1.5rem;
}

.export-live {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex: 1;
}

.export-bar-row .rim-progress {
  flex: 1;
}

/* 0.14s ease-out is Motion.quick, and opacity with a 0.96 scale is the
   transition the app hangs on the receipt itself. */
.export-live,
.export-receipt {
  transition: opacity 0.14s ease-out, transform 0.14s ease-out;
}

.export-receipt {
  position: absolute;
  left: 0;
  top: 50%;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  max-width: 100%;
  padding: 4px 6px;
  border-radius: 999px;
  /* glassCapsule. Measured off the app's own screenshot: the capsule sits
     about three percent of white above the plate behind it, with a hairline. */
  background: rgba(255, 255, 255, 0.05);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  transform: translateY(-50%) scale(0.96);
  opacity: 0;
  pointer-events: none;
}

.export-bar-row.is-done .export-live {
  opacity: 0;
  pointer-events: none;
}

.export-bar-row.is-done .export-receipt {
  opacity: 1;
  transform: translateY(-50%) scale(1);
  pointer-events: auto;
}

.export-receipt-frame {
  display: block;
  width: 30px;
  height: 20px;
  border-radius: 4px;
  object-fit: cover;
  flex: none;
}

/* The one place on the site that is not monochrome, and it is not a decision
   about the brand: this is a picture of the app's own interface, and the app
   marks a finished export in green. The value is sampled from the screenshot
   three tiles up, which shows the very same row. */
.export-receipt-done {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  font-weight: 600;
  color: #64cd57;
  white-space: nowrap;
}

/* The app's summary is .callout.monospacedDigit(): SF Pro with tabular
   figures, not a monospaced face. Setting it in the site's mono spaced the
   letters of 1920x1080 and MB out too and made the row a good deal wider than
   the app's. This is the body face with the figures locked. */
.export-receipt-summary {
  font-family: var(--sans);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
  padding-right: 4px;
  /* The app puts lineLimit(1) on this line, so on a narrow window the facts
     are cut rather than the row being pushed out of shape. Same here: the
     capsule never leaves the panel and the summary gives up its tail first. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.export-receipt-rule {
  width: 1px;
  height: 16px;
  background: var(--rule);
  flex: none;
}

/* FSReceiptActionStyle at rest: a 28 x 24 hit area with a 7 point corner and
   a secondary glyph. Nothing here is a control, so there is no hover state to
   go with it. */
.export-receipt-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 24px;
  border-radius: 7px;
  color: var(--muted);
  flex: none;
}

@media (prefers-color-scheme: light) {
  .export-receipt {
    background: rgba(11, 11, 12, 0.04);
    box-shadow: inset 0 0 0 1px rgba(11, 11, 12, 0.10);
  }

  /* Same reason as the dark value, one step down: the app's green on a light
     plate is the darker of the pair, and the lighter one is unreadable here. */
  .export-receipt-done {
    color: #248a3d;
  }
}

@media (prefers-reduced-motion: reduce) {
  .export-live,
  .export-receipt {
    transition: none;
  }
}



.export-bar-label {
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The bar beside it is flex: 1, so anything that changes this element's width
   changes the bar's width, which would resize the canvas and rebuild four
   render targets. The figures are tabular and the box is wide enough for the
   longest reading in any of the seven languages, %100 and 100%, so the width
   is fixed whatever the number says. */
.export-bar-pct {
  display: inline-block;
  min-width: 4ch;
  margin-left: 0.55rem;
  text-align: right;
  color: var(--ink);
}

.format-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
}

.format-card {
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: 1.1rem 1.2rem;
  background: var(--ground);
}

.format-card h3 {
  font-size: 1rem;
  margin-bottom: 0.4rem;
}

.format-card ul {
  margin: 0.6rem 0 0;
  padding-left: 1.1rem;
  color: var(--muted);
  font-size: 0.9rem;
}

.format-card li {
  margin-bottom: 0.3rem;
}

.filename-example {
  margin-top: 1.5rem;
  padding: 0.9rem 1.1rem;
  border: 1px dashed var(--rule-strong);
  border-radius: 10px;
  background: var(--ground);
}

.filename-example code {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--ink);
  word-break: break-all;
}

.filename-example p {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: var(--faint);
}

/* ── Steps timeline ─────────────────────────────────────
   Same class contract as the sibling sites so js/steps3d.js is portable.
   ─────────────────────────────────────────────────────── */

.steps-timeline {
  position: relative;
  margin-top: 2.5rem;
  padding-left: 3.25rem;
}

.timeline-track {
  position: absolute;
  left: 1.09rem;
  top: 0.6rem;
  bottom: 0.6rem;
  width: 2px;
  background: var(--rule);
  border-radius: 2px;
}

.timeline-track::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: var(--track-progress, 0%);
  border-radius: 2px;
  background: linear-gradient(180deg, var(--rim-1), var(--rim-2), var(--rim-3), var(--rim-4));
  transition: height 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.timeline-step {
  position: relative;
  padding-bottom: 2rem;
  opacity: 0;
  transform: translateY(24px);
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.4s ease;
}

.timeline-step:last-child {
  padding-bottom: 0;
}

.timeline-step.visible {
  opacity: 1;
  transform: translateY(0);
}

.step-number {
  position: absolute;
  left: -3.25rem;
  top: -0.1rem;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  border: 1px solid var(--rule-strong);
  background: var(--ground);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--muted);
}

.timeline-step.visible .step-number {
  color: var(--ink);
  border-color: var(--ink);
}

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

.step-content strong {
  color: var(--ink);
  font-weight: 620;
}

/* ── Spec table ────────────────────────────────────────── */

.scroller {
  overflow-x: auto;
  margin-top: 2rem;
}

table.spec {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}

table.spec th,
table.spec td {
  text-align: left;
  padding: 0.8rem 1rem;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}

table.spec th {
  font-weight: 560;
  white-space: nowrap;
  width: 34%;
  color: var(--ink);
}

table.spec td {
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.88rem;
}

table.spec tr:last-child th,
table.spec tr:last-child td {
  border-bottom: 0;
}

/* ── Support and info cards ────────────────────────────── */

.support-section {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: clamp(1.5rem, 4vw, 2.5rem);
  background: var(--raised);
}

.info-cards {
  /* Sits under the support card in the same section, so it carries the gap
     itself rather than leaning on two sections' padding. Matches the gap
     between the two cards it holds. */
  margin-top: 1.25rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: 1.25rem;
  margin-top: 2rem;
}

.info-card {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.5rem;
  background: var(--raised);
}

.info-card h3 {
  margin-bottom: 1rem;
}

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

.info-card li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: space-between;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: 0.92rem;
}

.info-card li:last-child {
  border-bottom: 0;
}

.info-label {
  color: var(--faint);
}

.info-value {
  color: var(--muted);
  text-align: right;
}

.disclaimer {
  margin-top: 2rem;
  font-size: 0.82rem;
  color: var(--faint);
  line-height: 1.6;
}

/* ── Sub page heads and prose ──────────────────────────── */

.subhead {
  text-align: center;
  padding-top: clamp(4.5rem, 9vw, 6.5rem);
  padding-bottom: 2rem;
}

/* The way back to the front page from a sub page. It is the app icon, linked,
   the way the family does it, and it lights on hover so it reads as a target
   rather than as decoration. The scale and the five degree tilt are the
   family's own numbers. */
/* The way back to the front page from a sub page: the icon, linked. Same size
   as the front page's, and the same band answers the pointer. */
.subhead-icon {
  display: block;
  width: var(--app-icon-size);
  margin: 0 auto 1.5rem;
}

.page-label {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: 0.85rem;
}

.prose h2 {
  margin-top: 2.75rem;
  margin-bottom: 0.85rem;
}

.prose h3 {
  margin-top: 1.75rem;
  margin-bottom: 0.5rem;
}

.prose p,
.prose li {
  color: var(--muted);
}

.prose ul {
  margin: 0 0 1.1em;
  padding-left: 1.2rem;
}

.prose li {
  margin-bottom: 0.4rem;
}

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

.faq-item {
  padding: 1.35rem 0;
  border-bottom: 1px solid var(--rule);
}

.faq-item:last-child {
  border-bottom: 0;
}

.faq-item h3 {
  margin-bottom: 0.5rem;
}

.faq-item p {
  color: var(--muted);
  margin: 0;
}

/* ── Footer ────────────────────────────────────────────── */

footer {
  padding-bottom: 3rem;
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding-top: 2.5rem;
}

.footer-inner p {
  margin: 0;
  color: var(--faint);
  font-size: 0.88rem;
}

.footer-lockup picture {
  width: 6.5rem;
}

.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer-nav a {
  color: var(--faint);
  font-size: 0.88rem;
}

.footer-nav a:hover,
.footer-nav a:focus-visible {
  color: var(--ink);
}

/* ── Responsive ────────────────────────────────────────── */

@media (max-width: 768px) {
  .speed-list li {
    grid-template-columns: 3rem 1fr;
  }

  .speed-list .speed-note {
    grid-column: 1 / -1;
    text-align: left;
  }

  .footer-inner {
    flex-direction: column;
    align-items: flex-start;
  }

  .hero-copy {
    text-align: left;
  }

  /* A narrow screen has no room for an 80px band on each side. Same value the
     sibling sites drop to. */
  .gallery-scroll-wrapper::before,
  .gallery-scroll-wrapper::after {
    width: 24px;
  }

  .custom-scrollbar-track,
  .gallery-section:hover .custom-scrollbar-track {
    height: 8px;
  }
}

@media (max-width: 480px) {
  .steps-timeline {
    padding-left: 2.75rem;
  }

  .step-number {
    left: -2.75rem;
    width: 1.9rem;
    height: 1.9rem;
    font-size: 0.78rem;
  }

  .timeline-track {
    left: 0.92rem;
  }
}

/* ── Reduce Motion ─────────────────────────────────────────
   The app turns its own band off under this setting, calling it an ambient
   effect the preference is meant to calm. The site does the same: the
   spectrum stays, it simply stops moving.
   ─────────────────────────────────────────────────────── */

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

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .gridscroll img:hover {
    transform: none;
  }
}
