/* ═══════════ DeskProxy marketing site ═══════════ */

/* ── design tokens ──
   One neutral ramp, one accent. Every surface, hairline and text tone is a
   step on a scale rather than a hand-picked hex, which is the whole reason
   the page reads as one system instead of forty decisions. The neutrals all
   sit on the same cool hue (~222°) so stacked surfaces never colour-shift
   against each other — the failure mode that makes a dark UI look muddy. */
:root {
  /* surfaces — each step is a real, perceptible lift, no accidental twins */
  --bg: #08090c;
  --bg-sunken: #050609;
  --bg-raise: #0f1217;
  --bg-card: #13161c;
  --bg-hover: #181c23;

  /* hairlines — three weights, so "resting" and "hovered" differ without
     jumping straight to the accent on every card in the page */
  --line-soft: #191d24;
  --line: #23272f;
  --line-strong: #333a45;

  /* text — body copy is HIGH contrast on purpose (Apple's rule: only labels
     and captions get to be quiet). --text-faint is the floor; anything
     smaller or lighter than this fails 4.5:1 on --bg. */
  --text: #f4f6f8;
  --text-dim: #a5adba;
  --text-faint: #7c8492;

  /* accent — one hue, three stops. -hi for hover, -lo for pressed. */
  --accent: #ff6a29;
  --accent-hi: #ff8348;
  --accent-lo: #e35916;
  --accent-soft: rgba(255, 106, 41, 0.10);
  --accent-line: rgba(255, 106, 41, 0.34);

  --green: #3ecf8e;
  --red: #ff5c5c;

  /* ── type scale ──
     Fluid, one ratio (≈1.25 at the small end opening to ≈1.33 at display
     sizes). Sizes are tokens so a heading and the paragraph under it can
     never drift to values that were never in the scale. */
  --fs-xs: 12px;
  --fs-sm: 13.5px;
  --fs-base: clamp(15px, 0.35vw + 14px, 16px);
  --fs-md: clamp(16px, 0.4vw + 15px, 17.5px);
  --fs-lg: clamp(17.5px, 0.6vw + 16px, 20px);
  --fs-xl: clamp(20px, 1vw + 17px, 26px);
  --fs-h3: clamp(19px, 0.9vw + 16px, 23px);
  --fs-h2: clamp(32px, 3.6vw + 12px, 64px);
  --fs-h1: clamp(40px, 5vw + 14px, 78px);

  /* Optical tracking: big type needs negative tracking to stop looking loose,
     small caps-y type needs positive to stop looking jammed. One value for
     the whole page is why most sites' headlines read as "default". */
  --track-display: -0.033em;
  --track-head: -0.022em;
  --track-body: -0.005em;
  --track-caps: 0.1em;

  /* ── spacing scale (4px base) ── */
  --s1: 4px;   --s2: 8px;   --s3: 12px;  --s4: 16px;
  --s5: 20px;  --s6: 24px;  --s7: 32px;  --s8: 40px;
  --s9: 52px;  --s10: 68px; --s11: 88px; --s12: 120px;

  /* Section rhythm and page gutter, both fluid. Every band on the page uses
     these two — inconsistent section padding is the single most common tell
     that a page was assembled rather than designed. */
  --section-y: clamp(96px, 10vw, 168px);
  --gutter: clamp(20px, 4vw, 32px);

  --radius: 14px;
  --radius-lg: 20px;
  --radius-xl: 26px;

  /* ── motion ──
     Two easings and three durations, everywhere. --ease-out is the decel
     curve UI motion should almost always use; --ease-spring overshoots
     very slightly and is reserved for entrances. */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
  --t-fast: 140ms;
  --t: 220ms;
  --t-slow: 420ms;

  /* ── elevation ──
     Dark-UI shadows work by being large, soft and nearly black; a "lifted"
     card also gets a 1px top highlight, which is what actually sells the
     lift on a dark background. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-md: 0 12px 32px -8px rgba(0, 0, 0, .6);
  --shadow-lg: 0 32px 72px -16px rgba(0, 0, 0, .7);
  --shadow-glow: 0 8px 28px -6px rgba(255, 106, 41, .38);
  --edge-hi: inset 0 1px 0 rgba(255, 255, 255, .05);

  /* ── glass ──
     Cinematic surfaces are not opaque panels; they are translucent sheets
     sitting in front of a lit backdrop, which is what gives the page depth
     instead of the flat "cards on a background" look. The tint is white at
     very low alpha so a surface picks up whatever the wash behind it is
     doing, rather than fighting it with a colour of its own. */
  --glass: rgba(255, 255, 255, .035);
  --glass-strong: rgba(255, 255, 255, .055);
  --glass-line: rgba(255, 255, 255, .085);
  --glass-line-hi: rgba(255, 255, 255, .14);
  --blur: saturate(150%) blur(22px);
/* ── why these surfaces are translucent but NOT blurred ──
   backdrop-filter over a position:fixed backdrop has to resample on every
   scroll frame, per element — and there are twenty-odd cards here. Blur only
   buys anything over backdrop DETAIL, and the wash behind these is a soft
   gradient with no high frequencies to smear. So the glass keeps its
   translucency (which is what actually picks up the light) and drops the
   blur. Blur stays only where real content passes underneath: the nav. */


  /* ── the wash ──
     Three stops of one sunset-to-night ramp. NOT three unrelated hues: warm
     sits at the accent, indigo is its complement, rose is the midpoint that
     keeps the transition between them from passing through mud. Sections
     claim one each, and the backdrop crossfades as you scroll. */
  --wash-warm: 255, 106, 41;
  --wash-rose: 226, 74, 110;
  --wash-indigo: 78, 86, 214;

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter",
          "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Menlo", monospace;
  --container: 1280px;
}

/* ultra-wide: let it breathe */
@media (min-width: 1800px) {
  :root { --container: 1440px; }
}

* { margin: 0; padding: 0; box-sizing: border-box; }
/* scroll-padding clears the sticky nav: without it every in-page anchor
   (#tiles, #voice, #hardware…) parks its heading underneath the bar. */
html { scroll-behavior: smooth; scroll-padding-top: 96px; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-base);
  line-height: 1.65;
  letter-spacing: var(--track-body);
  overflow-x: hidden;
  position: relative;
  /* The single largest "is this a professional site?" lever on a dark
     background. Without it macOS renders light-on-dark text with a synthetic
     bold smear that no amount of type scale fixes. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-optical-sizing: auto;
}

/* ── ambient backdrop: fixed glow field + faint grid, behind everything ──
   Two layers so the grid stays crisp while the glows stay soft.

   Deliberately ONE warm source plus a single cool counterweight. The old
   backdrop ran orange + blue + green at once, which is three hues competing
   under content that already carries an accent — the result reads as tinted
   rather than lit. A light source has a direction; this one comes from the
   top right, which is also where the hero device sits. */
body::before,
body::after { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: -1; }
/* The static floor only. The lit part of the backdrop is the .wash element
   below, because it has to crossfade — and a pseudo-element's background
   cannot be transitioned between two gradients. */
body::before {
  background: linear-gradient(180deg, transparent 55%, rgba(0, 0, 0, .5) 100%);
}
body::after {
  background-image:
    linear-gradient(rgba(255, 255, 255, .016) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .016) 1px, transparent 1px);
  background-size: 72px 72px;
  /* fade the grid out toward the bottom so it never fights the content */
  mask-image: linear-gradient(180deg, #000 0%, #000 38%, transparent 82%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 38%, transparent 82%);
}

/* ═══════════ THE WASH ═══════════
   A fixed, three-layer light field behind the whole page. Each section
   declares which layer it wants lit (data-wash in the markup); js/main.js
   crossfades between them as you scroll, so the backdrop drifts from warm to
   rose to indigo over the length of the page and back.

   Why three stacked elements rather than one animated gradient: a background
   image cannot be transitioned between two values, and interpolating colours
   through custom properties needs @property support that Safari only
   recently shipped. Three layers with transitioned OPACITY is the version
   that works everywhere and composites on the GPU. */
.wash {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  overflow: hidden;
}
.wash-layer {
  /* inset:0, not a negative inset. Blowing the layer out past the viewport
     also moves the gradient's percentage origin outward, which pushed the
     light source off-screen and left the page looking merely tinted. */
  position: absolute; inset: 0;
  opacity: 0;
  /* Slow on purpose. A backdrop that keeps up with the scroll reads as a
     gimmick; one that lags behind it reads as light in a room. */
  transition: opacity 1400ms var(--ease);
  will-change: opacity;
}
.wash-layer.on { opacity: 1; }

/* Each layer is the same composition lit from a different side, so the
   crossfade moves the light across the page rather than just recolouring it. */
/* Two sources per layer, both well inside the frame so the falloff is
   visible on screen rather than happening past the edge. The key light
   carries the mood; the fill keeps the opposite corner from going dead. */
.wash-layer[data-wash="warm"] {
  background:
    radial-gradient(70% 55% at 78% 8%, rgba(var(--wash-warm), .30), transparent 68%),
    radial-gradient(60% 50% at 6% 62%, rgba(var(--wash-indigo), .18), transparent 66%);
}
.wash-layer[data-wash="rose"] {
  background:
    radial-gradient(72% 56% at 16% 14%, rgba(var(--wash-rose), .26), transparent 68%),
    radial-gradient(64% 52% at 90% 70%, rgba(var(--wash-warm), .22), transparent 66%);
}
.wash-layer[data-wash="indigo"] {
  background:
    radial-gradient(74% 58% at 68% 6%, rgba(var(--wash-indigo), .30), transparent 68%),
    radial-gradient(58% 50% at 10% 78%, rgba(var(--wash-rose), .16), transparent 66%);
}

/* Grain. Half a percent of noise over a large soft gradient is what stops it
   banding into visible stripes on an 8-bit panel — and it is most of why a
   cinematic backdrop looks photographed rather than rendered. */
.wash::after {
  content: ""; position: absolute; inset: 0;
  opacity: .10; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* keyboard users get a visible ring everywhere */
a:focus-visible, button:focus-visible, input:focus-visible,
summary:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 6px;
}

/* ── headings ──
   Tracking tightens as size grows, which is how type behaves optically.
   text-wrap: balance stops the one-word last line that makes an otherwise
   good headline look accidental. */
h1, h2, h3 {
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: var(--track-head);
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); letter-spacing: var(--track-display); }
h2 { font-size: var(--fs-h2); letter-spacing: var(--track-display); }
h3 { font-size: var(--fs-h3); line-height: 1.25; }
/* Avoids the widowed last word in body copy. Ignored where unsupported. */
p { text-wrap: pretty; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; }
section { padding: var(--section-y) var(--gutter); max-width: var(--container); margin: 0 auto; }

.kicker {
  display: inline-block;
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--accent);
  margin-bottom: var(--s3);
}
.section-head { text-align: center; margin-bottom: var(--s10); }
/* Build's head is only kicker + h2 now — the prose moved into the split. */
.build > .section-head { margin-bottom: var(--s9); }
.section-sub {
  color: var(--text-dim); max-width: 64ch; margin: var(--s5) auto 0;
  font-size: var(--fs-lg); line-height: 1.6;
}
.section-sub strong { color: var(--text); font-weight: 600; }

/* ── reveal-on-scroll ──
   Shorter travel and a faster curve than before. A 28px/700ms rise reads as
   a page still loading; 14px/460ms reads as the content settling. Entrances
   are the one place the slight overshoot of --ease-spring belongs. */
/* Adds a focus pull to the rise: content resolves out of a soft blur the way
   a lens finds it, which is the single cheapest way to make a page feel shot
   rather than laid out. filter is GPU-composited, so this stays cheap. */
.reveal {
  opacity: 0; transform: translateY(18px) scale(.985); filter: blur(6px);
  transition: opacity 620ms var(--ease-out),
              transform 720ms var(--ease-spring),
              filter 620ms var(--ease-out);
}
.reveal.in { opacity: 1; transform: none; filter: none; }
.reveal[data-delay="1"] { transition-delay: 60ms; }
.reveal[data-delay="2"] { transition-delay: 120ms; }
.reveal[data-delay="3"] { transition-delay: 180ms; }
/* Grid children stagger themselves — a row of four cards arriving together
   reads as one block appearing; 55ms apart reads as a deal being dealt. */
.use-grid > .reveal:nth-child(2), .lanes > .reveal:nth-child(2) { transition-delay: 55ms; }
.use-grid > .reveal:nth-child(3), .lanes > .reveal:nth-child(3) { transition-delay: 110ms; }
.use-grid > .reveal:nth-child(4), .lanes > .reveal:nth-child(4) { transition-delay: 165ms; }

/* ── buttons ── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  padding: 13px 24px; border-radius: 999px; min-height: 46px;
  font: inherit; font-weight: 600; font-size: var(--fs-base);
  letter-spacing: -0.01em; white-space: nowrap;
  border: 1px solid transparent; cursor: pointer;
  transition: transform var(--t-fast) var(--ease-out),
              box-shadow var(--t) var(--ease-out),
              background-color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}
/* 1px, not 2 — and an :active that returns to zero. A hover that lifts but
   never presses back is the uncanny half of a button. */
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); transition-duration: 60ms; }
.btn-primary {
  /* Dark ink on the accent, not white. White on #ff6a29 is 2.9:1 — it fails
     4.5:1 and always looked slightly hazy; near-black is 6.2:1 and is what
     every well-made bright-accent button actually does. */
  background: var(--accent); color: #1a0b02;
  box-shadow: var(--shadow-glow), var(--edge-hi);
}
.btn-primary:hover { background: var(--accent-hi); box-shadow: 0 12px 34px -6px rgba(255, 106, 41, .5), var(--edge-hi); }
.btn-primary:active { background: var(--accent-lo); box-shadow: var(--shadow-sm); }
.btn-ghost { border-color: var(--line); color: var(--text-dim); background: transparent; }
.btn-ghost:hover { color: var(--text); border-color: var(--line-strong); background: var(--bg-raise); }
.btn-ghost:active { background: var(--bg-card); }

/* ═══════════ NAV ═══════════ */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  backdrop-filter: saturate(160%) blur(20px);
  -webkit-backdrop-filter: saturate(160%) blur(20px);
  background: rgba(8, 9, 12, 0.32);
  border-bottom: 1px solid transparent;
  transition: background .3s ease, border-color .3s ease, box-shadow .3s ease;
}
.nav.scrolled {
  background: rgba(8, 9, 12, 0.82);
  border-bottom-color: var(--line-soft);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .03), 0 8px 30px rgba(0, 0, 0, .4);
}
.nav-inner {
  max-width: var(--container); margin: 0 auto;
  padding: var(--s3) var(--gutter);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
}
.nav-logo { display: flex; align-items: center; }
/* wordmark is a 352×60 SVG — height-constrained, width follows */
.nav-wordmark { height: 28px; width: auto; display: block; }
.nav-links {
  display: flex; gap: var(--s6); align-items: center;
  font-size: var(--fs-sm); color: var(--text-dim);
}
/* Link rows get a hit box, not just text: 14px glyphs with no padding are a
   4px-tall target in practice. */
.nav-links a {
  padding: 8px 2px; transition: color var(--t-fast) var(--ease);
  position: relative;
}
.nav-links a:hover { color: var(--text); }
/* Scroll-spy: the section you are actually in gets marked. On a page with
   thirteen bands, a nav that never says where you are is decoration. */
.nav-links a.is-current { color: var(--text); }
.nav-links a.is-current::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 2px; height: 1.5px;
  background: var(--accent); border-radius: 2px;
}
.nav-links .nav-cta::after { display: none; }
.nav-cta {
  color: #1a0b02 !important; font-weight: 600;
  background: var(--accent); border-radius: 999px;
  padding: 9px 16px !important; margin-left: var(--s1);
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease-out);
}
.nav-cta:hover { background: var(--accent-hi); transform: translateY(-1px); }
.nav-cta:active { background: var(--accent-lo); transform: none; }

/* ── mobile menu ──
   Below 900px the seven section links used to be `display:none` with nothing
   replacing them, which left a thirteen-section page with no wayfinding at
   all on a phone. This is a plain disclosure: a real <button> with
   aria-expanded, a panel that traps nothing and closes on Escape, on
   selection, and on resize back to desktop. */
.nav-toggle { display: none; }
.nav-toggle {
  width: 42px; height: 42px; flex: none;
  align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--line); border-radius: 12px;
  color: var(--text); cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.nav-toggle:hover { border-color: var(--line-strong); background: var(--bg-raise); }
.nav-toggle-bars { display: block; width: 17px; height: 11px; position: relative; }
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: ""; position: absolute; left: 0; right: 0; height: 1.5px;
  background: currentColor; border-radius: 2px;
  transition: transform var(--t) var(--ease-out), top var(--t) var(--ease-out);
}
.nav-toggle-bars::before { top: 0; }
.nav-toggle-bars::after { top: 9.5px; }
/* Bars become an X on open — the affordance that says "this closes again". */
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before { top: 4.75px; transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after { top: 4.75px; transform: rotate(-45deg); }

/* ═══════════ HERO ═══════════ */
/* svh, not vh: on iOS `100vh` measures the viewport WITHOUT the browser
   chrome, so the hero was always taller than the screen and the fold sat
   under the URL bar. */
.hero {
  min-height: min(100svh, 900px);
  display: flex; align-items: center;
  padding: 132px var(--gutter) var(--s11);
  max-width: var(--container); margin: 0 auto;
}
/* 1.05fr/1fr, not 1fr/1fr: the copy column carries a 68px headline and needs
   marginally more room than a fixed-width device prop. */
.hero-sticky {
  display: grid; grid-template-columns: 1.05fr 1fr;
  gap: clamp(40px, 5vw, 72px); align-items: center; width: 100%;
}
.hero-copy h1 {
  margin-bottom: var(--s6);
  /* faint top-down fade keeps the headline from sitting flat on the dark bg */
  background: linear-gradient(180deg, #fff 30%, #b9c2d0 100%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
/* Quiet setup, bright payoff: the first clause steps back so "finally paying
   attention" lands. Both halves stay in the same neutral family — the tonal
   drop does the work, no second hue. -webkit-text-fill-color is required
   because the h1 above paints through a clipped gradient. */
.hero-copy h1 .h1-quiet {
  color: #79828f;
  -webkit-text-fill-color: #79828f;
}
.hero-copy p { color: var(--text-dim); font-size: var(--fs-md); max-width: 48ch; margin-bottom: var(--s7); }
.hero-actions { display: flex; gap: var(--s3); flex-wrap: wrap; }

/* device — real daemon-rendered frames, cycled by js/main.js */
/* Float halved and slowed: a 14px bob on a 6s cycle is visible motion in
   peripheral vision the entire time the hero is on screen, which is
   distracting rather than alive. 6px over 9s reads as breathing. */
.hero-device {
  position: relative; display: flex; justify-content: center;
  animation: float 9s var(--ease) infinite;
}
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
.device-frame {
  position: relative;
  width: min(420px, 84vw);
  /* Warmed the highlight edge and deepened the body so the bezel reads as a
     machined object under the page's top-right light source, rather than as
     a grey rectangle. */
  background: linear-gradient(158deg, #262b33 0%, #1b1f26 46%, #121519 100%);
  border: 1px solid #31373f; border-radius: var(--radius-xl); padding: 17px;
  box-shadow:
    var(--shadow-lg),
    0 0 140px -20px rgba(255, 106, 41, .16),
    inset 0 1px 0 rgba(255, 255, 255, .07),
    inset 0 -1px 0 rgba(0, 0, 0, .5);
}
.device-screen {
  position: relative; aspect-ratio: 3 / 2; border-radius: 12px; overflow: hidden;
  background: #0a0e15;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .8), inset 0 2px 12px rgba(0, 0, 0, .6);
}
/* glass glare — a fixed diagonal sheen over whichever frame is showing */
.device-screen::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(115deg, rgba(255, 255, 255, .10) 0%, rgba(255, 255, 255, .03) 22%, transparent 46%);
}
/* stacked frames; only .is-on is visible, so swaps cross-fade */
.device-shot {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; transition: opacity .7s ease;
}
.device-shot.is-on { opacity: 1; }
/* ── front-edge RGB light bar ──
   Six WS2812s behind a diffuser read as one continuous lit edge at this size,
   so this draws the strip rather than the individual emitters. Pinned to the
   frame's bottom lip; the colour drifts slowly instead of blinking, because
   the real strip is a notification channel and the page says so. Injected by
   js/main.js. */
.device-leds {
  /* Flush with the bottom lip — the strip is on the device's front edge, so
     any gap below it reads as a floating stripe instead of a lit edge. The
     side insets follow the frame's corner radius so it doesn't poke out. */
  position: absolute; left: 26px; right: 26px; bottom: 3px; height: 4px;
  border-radius: 999px; overflow: hidden;
  background: linear-gradient(90deg,
    #ff6b2c, #ffb340, #4ad991, #3ba9ff, #a06bff, #ff5f8f, #ff6b2c);
  background-size: 300% 100%;
  /* Was .75, which made a six-colour rainbow the most saturated thing on a
     page whose whole palette is one orange. The strip is real hardware and
     stays, but it should read as a lit edge, not as the hero's focal point. */
  opacity: .58;
  animation: led-drift 18s linear infinite;
}
/* Bloom under the lip — the light spills onto the desk, it doesn't stop at
   the plastic. */
.device-leds::after {
  content: ""; position: absolute; inset: -6px -2px -10px;
  background: inherit; background-size: inherit;
  filter: blur(7px); opacity: .55; border-radius: 999px;
}
@keyframes led-drift { to { background-position: 300% 0; } }

/* A drifting rainbow is a poor thing to force on anyone; hold it still. */
@media (prefers-reduced-motion: reduce) {
  .device-leds, .device-leds::after { animation: none; }
}

/* ── bezel wordmark ──
   Replaces the old grey stand nub: a chin with the product name printed on it
   reads as a finished device, a blank tab reads as an unlabelled prototype.
   Deliberately NOT the SVG wordmark — that one carries terminal chrome (~$, a
   blinking cursor) which belongs on a screen, not silkscreened on plastic.
   Static and low-contrast, the way a real printed mark sits. */
.device-badge {
  display: flex; justify-content: center; align-items: baseline; gap: 2px;
  margin: 16px auto 0; font-size: 11px; letter-spacing: .08em;
  color: rgba(255, 255, 255, .5); user-select: none;
}
.device-badge b { font-weight: 700; }
.device-badge span { font-family: var(--mono, ui-monospace, Menlo, monospace); font-weight: 500; }

/* ── frame indicator under the device ──
   These are real buttons now, not decorative <i>s. A control that shows
   position but refuses to accept a click is the most irritating shape a
   carousel indicator can take. The pip stays 6px; the padding grows the hit
   box to the 24px WCAG 2.5.8 minimum via content-box clipping. */
.device-dots {
  position: absolute; bottom: -40px; left: 0; right: 0;
  display: flex; justify-content: center; gap: 2px;
}
.device-dots button {
  width: 6px; height: 6px; padding: 9px 6px; box-sizing: content-box;
  border: 0; background: var(--line); background-clip: content-box;
  border-radius: 999px; cursor: pointer;
  transition: background var(--t) var(--ease), width var(--t) var(--ease-out);
}
.device-dots button:hover { background: var(--text-faint); background-clip: content-box; }
.device-dots button.on { background: var(--accent); background-clip: content-box; width: 18px; }
.device-dots button:focus-visible { outline-offset: -2px; }

/* ── "Works with" strip under the hero copy ── */
.works-with { margin-top: var(--s8); padding-top: var(--s6); border-top: 1px solid var(--line-soft); display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; }
.works-with-label {
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-faint); font-weight: 600;
}
.works-with ul { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; }
.works-with li {
  font-size: 13px; color: var(--text-dim);
  border: 1px solid var(--line); border-radius: 999px; padding: 5px 12px;
  white-space: nowrap;
  display: inline-flex; align-items: center; gap: 8px;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.works-with li:hover { color: var(--text); border-color: var(--line-strong); background: var(--bg-raise); }

/* Brand marks. The flat colour is baked into the SVG files themselves, not
   applied here — CSS masking was the obvious route but Safari drops the
   `mask: <url> center / contain` shorthand on parse and leaves a solid box.
   A plain <img> renders everywhere. Brightness carries the hover instead. */
.works-with .wm {
  width: 17px; height: 17px; flex: none;
  display: block;
  transition: filter .2s ease;
}
.works-with li:hover .wm { filter: brightness(1.45); }

/* Marks-only mode: names stay in the DOM (screen readers, SEO) but leave the
   layout. Remove .marks-only from the <ul> to bring the labels back. */
.works-with .marks-only { gap: 10px; }
.works-with .marks-only li { padding: 7px 11px; }
.works-with .marks-only .wm-name {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* Text-only pills (no mark licensed) sit in the same row and keep their label. */
.works-with .marks-only li.wm-text .wm-name {
  position: static; width: auto; height: auto; overflow: visible;
  clip-path: none;
}
.works-with .marks-only li.wm-text { padding: 6px 12px; }

/* ═══════════ EXPLODED HARDWARE ═══════════ */
.exploded { display: grid; grid-template-columns: 1fr 1fr; gap: 72px; align-items: center; }
.exploded-stack { display: flex; flex-direction: column; gap: 22px; perspective: 900px; }
.layer {
  height: 92px; border-radius: var(--radius); position: relative;
  transform: rotateX(55deg) rotateZ(-8deg); transform-style: preserve-3d;
  border: 1px solid var(--line);
  transition: transform 0.4s ease;
}
.layer:hover { transform: rotateX(48deg) rotateZ(-8deg) translateZ(12px); }
.layer-glass { background: linear-gradient(135deg, rgba(120, 180, 255, 0.14), rgba(120, 180, 255, 0.04)); backdrop-filter: blur(2px); }
.layer-display { background: linear-gradient(135deg, #14263e, #0d1522); }
.layer-board { background: linear-gradient(135deg, #14261b, #0d1a12); }
.layer-back { background: linear-gradient(135deg, #23262c, #141519); }
.layer-tag {
  position: absolute; right: -12px; top: 50%;
  transform: rotateZ(8deg) rotateX(-55deg) translateY(-50%);
  font-size: 13px; color: var(--text-dim); white-space: nowrap;
  background: var(--bg-raise); border: 1px solid var(--line);
  padding: 5px 12px; border-radius: 999px;
}
.spec-list { list-style: none; display: flex; flex-direction: column; gap: 10px; }
.spec-list strong { display: block; font-size: var(--fs-md); margin-bottom: var(--s1); letter-spacing: var(--track-body); }
.spec-list span { color: var(--text-dim); font-size: var(--fs-base); }

/* ── exploded view: two-way highlight ──
   No 3D model to explode, so the stack and the spec list point at each other
   instead: hovering either end lights both. That is the part an exploded
   render actually communicates — which words belong to which layer — and it
   costs nothing to ship. Driven by data-spec pairs in js/main.js. */
.spec-list li {
  padding: var(--s3) var(--s4); margin-left: calc(var(--s4) * -1);
  border-left: 2px solid transparent; border-radius: 0 var(--radius) var(--radius) 0;
  transition: background 0.2s ease, border-color 0.2s ease;
  cursor: default;
}
.spec-list li.is-lit,
.spec-list li:focus-visible {
  background: var(--bg-raise); border-left-color: var(--accent);
}
.layer.is-lit {
  transform: rotateX(48deg) rotateZ(-8deg) translateZ(12px);
  border-color: var(--accent);
}
.layer.is-lit .layer-tag { border-color: var(--accent); color: var(--text); }
/* Dim the layers you are NOT pointing at, so the lit one reads as selected
   rather than merely nudged. */
.exploded-stack.has-lit .layer:not(.is-lit) { opacity: 0.45; }
.layer, .layer-tag { transition: transform 0.4s ease, opacity 0.25s ease, border-color 0.25s ease, color 0.25s ease; }

@media (prefers-reduced-motion: reduce) {
  .layer, .layer-tag { transition: opacity 0.25s ease, border-color 0.25s ease; }
  .layer.is-lit { transform: rotateX(55deg) rotateZ(-8deg); }
}

/* ═══════════ USES ═══════════ */
/* ── card system ──
   One rule for every card on the page (uses, privacy, lanes, status). The
   old hover slammed the border to full accent and lifted 4px on ten cards
   in a row, so a mouse crossing the section set off a chain of orange
   flashes. Restraint: 2px lift, the hairline steps up one weight, and the
   accent appears only as a faint wash at the top edge. */
.use-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--s4); }
.use-card {
  position: relative; isolation: isolate;
  /* Glass, not an opaque panel: the card is a sheet in front of the wash, so
     it picks up whatever light is behind it instead of punching a flat hole
     through the backdrop. */
  background: var(--glass); border: 1px solid var(--glass-line);
  border-radius: var(--radius-lg); padding: var(--s7) var(--s6);
  box-shadow: var(--edge-hi);
  transition: border-color var(--t) var(--ease),
              transform var(--t) var(--ease-out),
              background-color var(--t) var(--ease),
              box-shadow var(--t) var(--ease-out);
}
.use-card::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  border-radius: inherit; opacity: 0;
  background: radial-gradient(120% 70% at 50% -10%, var(--accent-soft), transparent 70%);
  transition: opacity var(--t) var(--ease);
}
.use-card:hover {
  border-color: var(--glass-line-hi); background: var(--glass-strong);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md), var(--edge-hi);
}
.use-card:hover::before { opacity: 1; }
.use-card h3 { font-size: var(--fs-h3); margin-bottom: var(--s2); }
.use-card p { color: var(--text-dim); font-size: var(--fs-base); }

/* ── hero: lede carries the pitch, sub carries the second beat ── */
.hero-copy .hero-lede {
  font-size: var(--fs-lg); color: var(--text); line-height: 1.55;
  margin-bottom: var(--s4);
}
.hero-copy .hero-lede strong { color: var(--accent); font-weight: 600; }
.hero-copy .hero-sub { font-size: var(--fs-base); margin-bottom: var(--s7); }

/* ── build: SDK pitch ──
   One block instead of prose beside cards. Bold lead-ins do the scanning work
   the card headings used to, without the boxes or the duplicated sentences. */
.build-brief { max-width: 76ch; margin: 0 auto var(--s10); }
.build-lede {
  color: var(--text-dim); font-size: var(--fs-md); line-height: 1.65;
  margin-bottom: var(--s6);
}
.build-points { list-style: none; display: grid; gap: var(--s4); }
.build-points li {
  position: relative; padding-left: 26px;
  color: var(--text-dim); font-size: var(--fs-base); line-height: 1.7;
}
/* Accent dot as the marker — list-style can't be coloured independently.
   Was a hollow bordered square, which read as an UNCHECKED checkbox next to
   a list of things the product already does. Same filled dot as the lane and
   app-tour lists, so all three bulleted lists on the page match. */
.build-points li::before {
  content: ""; position: absolute; left: 2px; top: .68em;
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 70%, transparent);
}
.build-points strong { color: var(--text); font-weight: 600; }

/* ── voice: card system, wider grid so six cards sit evenly ── */
.voice-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.voice-grid .use-card h3 { display: flex; gap: 10px; align-items: center; }

/* ── voice lanes ──
   Three ways in, deliberately shown as three separate columns rather than a
   card grid: the whole point of the section is that a command, a question and
   a meeting are NOT the same feature. auto-fit would let two sit on one row
   and imply a pairing, so the breakpoint goes straight from 3-up to stacked. */
/* auto-fit, not a fixed 3: dictation made this four lanes, and a hard column
   count strands the last card alone on its own row. */
.lanes { display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); gap: var(--s4); }
.lane {
  background: var(--glass); border: 1px solid var(--glass-line);
  border-radius: var(--radius-lg); padding: var(--s7) var(--s6);
  display: flex; flex-direction: column;
  box-shadow: var(--edge-hi);
  transition: border-color var(--t) var(--ease),
              transform var(--t) var(--ease-out),
              background-color var(--t) var(--ease),
              box-shadow var(--t) var(--ease-out);
}
.lane:hover {
  border-color: var(--glass-line-hi); background: var(--glass-strong);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md), var(--edge-hi);
}
/* The trigger chip is the answer to "how do I start this one?" — it reads
   before the heading on purpose, since that is what differs between lanes. */
.lane-trigger {
  align-self: flex-start;
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.03em;
  color: var(--accent); background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: 999px; padding: 5px 12px; margin-bottom: var(--s4);
}
.lane h3 { font-size: var(--fs-h3); margin-bottom: var(--s2); }
.lane > p { color: var(--text-dim); font-size: var(--fs-base); margin-bottom: var(--s5); }
.lane-notes { list-style: none; margin: auto 0 0; display: grid; gap: var(--s3); padding-top: var(--s4); border-top: 1px solid var(--line-soft); }
.lane-notes li {
  position: relative; padding-left: 18px;
  color: var(--text-dim); font-size: var(--fs-sm); line-height: 1.6;
}
.lane-notes li::before {
  content: ""; position: absolute; left: 0; top: 8px;
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 55%, transparent);
}
.lane-notes strong { color: var(--text); font-weight: 600; }
@media (max-width: 900px) { .lanes { grid-template-columns: 1fr; } }

/* ═══════════ MARQUEE ═══════════ */
.marquee-section { max-width: none; padding-left: 0; padding-right: 0; }
.marquee { overflow: hidden; padding: 14px 0; mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.marquee-track { display: flex; gap: 22px; width: max-content; }
.marquee-right .marquee-track { animation: scroll-l 48s linear infinite; }
.marquee-left  .marquee-track { animation: scroll-r 56s linear infinite; }
/* focus-within as well as hover: a keyboard user tabbing into the row has
   no way to stop a strip that is still sliding under them. */
.marquee:hover .marquee-track,
.marquee:focus-within .marquee-track { animation-play-state: paused; }
@keyframes scroll-l { from { transform: translateX(0); } to { transform: translateX(-33.333%); } }
@keyframes scroll-r { from { transform: translateX(-33.333%); } to { transform: translateX(0); } }

/* tile cards — real 480×320 daemon renders */
.tilecard {
  position: relative; flex-shrink: 0;
  width: 336px; aspect-ratio: 3 / 2;
  border: 1px solid var(--line); border-radius: 16px;
  overflow: hidden; background: var(--bg-sunken);
  box-shadow: var(--shadow-sm);
  transition: transform var(--t) var(--ease-out),
              border-color var(--t) var(--ease),
              box-shadow var(--t) var(--ease-out);
}
.tilecard img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 1.04 on a 336px card is a 13px jump — enough to shove its neighbours in
   peripheral vision. 1.02 plus a shadow reads as "picked up". */
.tilecard:hover {
  transform: scale(1.02); border-color: var(--line-strong);
  box-shadow: var(--shadow-md);
}
.tilecard-label {
  position: absolute; bottom: 0; left: 0; right: 0;
  padding: 22px 14px 8px; text-align: right;
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.85);
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
  opacity: 0; transition: opacity 0.2s ease; pointer-events: none;
}
.tilecard:hover .tilecard-label { opacity: 1; }
.tilecard-lg { width: min(560px, 100%); }

/* ═══════════ WALKTHROUGH ═══════════ */
.walkthrough { display: flex; flex-direction: column; gap: var(--s8); }

/* ── walkthrough carousel ──
   Slides are stacked in one grid cell rather than laid out in a row: the
   container then sizes to the TALLEST slide, so stepping between tiles with
   different copy lengths never makes the page jump under the reader. */
.wt-track { display: grid; }
.wt-track > .wt {
  grid-area: 1 / 1;
  opacity: 0; visibility: hidden;
  transition: opacity 0.35s ease;
}
.wt-track > .wt.is-active { opacity: 1; visibility: visible; }

.wt-nav { display: flex; align-items: center; justify-content: center; gap: var(--s5); margin-top: var(--s7); }
.wt-arrow {
  width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--glass); border: 1px solid var(--glass-line);
  color: var(--text); font-size: 17px; line-height: 1; cursor: pointer;
  box-shadow: var(--edge-hi);
  transition: border-color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease-out);
}
.wt-arrow:active { transform: scale(.94); background: var(--bg-card); }
.wt-arrow:hover { border-color: var(--glass-line-hi); background: var(--glass-strong); }
.wt-dots { display: flex; gap: 1px; flex-wrap: wrap; justify-content: center; }
/* 9×9px is a 9px tap target — WCAG 2.2 §2.5.8 asks for 24. content-box
   padding grows the hit box to 24×24 while the drawn pip stays 8px. */
.wt-dot {
  width: 8px; height: 8px; padding: 8px 6px; box-sizing: content-box;
  border-radius: 999px; cursor: pointer;
  background: var(--line); background-clip: content-box; border: 0;
  transition: background var(--t) var(--ease), width var(--t) var(--ease-out);
}
.wt-dot:hover { background: var(--text-faint); background-clip: content-box; }
/* The active dot stretches instead of only recolouring, so position in the
   set stays legible without relying on colour alone. */
.wt-dot.is-on { background: var(--accent); background-clip: content-box; width: 24px; }
.wt-dot:focus-visible { outline-offset: -3px; }
.wt-arrow:focus-visible, .wt-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .wt-track > .wt { transition: none; }
  .wt-arrow, .wt-dot { transition: none; }
}

.wt {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 4vw, 60px); align-items: center;
  padding: clamp(28px, 3.4vw, 48px);
  background: var(--glass);
  border: 1px solid var(--glass-line); border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg), var(--edge-hi);
}
.wt-flip .wt-copy { order: 2; }
.wt-copy h3 { font-size: clamp(24px, 2.4vw + 12px, 34px); letter-spacing: var(--track-head); margin-bottom: var(--s3); }
.wt-copy p { color: var(--text-dim); margin-bottom: var(--s5); max-width: 46ch; }
.wt-views { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s2); }
.wt-views li {
  font-size: var(--fs-xs); padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--glass-line); color: var(--text-dim);
  background: rgba(255, 255, 255, .03);
}
.wt-screen { display: flex; flex-direction: column; align-items: center; gap: var(--s4); }
.wt-thumbs { display: flex; gap: var(--s3); flex-wrap: wrap; justify-content: center; }
.wt-thumbs .tilecard { width: 150px; cursor: pointer; }

/* ═══════════ DAEMON APP ═══════════ */
.app-shot {
  border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden;
  box-shadow: var(--shadow-lg), var(--edge-hi);
}
.app-shot img { width: 100%; display: block; }

/* ── desktop-app screenshot tour ──
   Full container width: these are 1700px captures of a dense desktop app, so
   every pixel taken away is a label someone can't read. */
.app-tour { max-width: 100%; margin: 0 auto; }
/* ── segmented control ──
   Tabs sit in a track rather than floating as loose pills. It reads as one
   control with a selected item instead of five buttons that happen to be
   adjacent — and it gives the row an edge, which is what was missing. */
.app-tour-tabs {
  display: flex; gap: var(--s1); justify-content: center; flex-wrap: wrap;
  margin: 0 auto var(--s5); width: fit-content; max-width: 100%;
  padding: 5px; background: var(--glass);
  backdrop-filter: var(--blur);
  border: 1px solid var(--glass-line); border-radius: 999px;
  box-shadow: var(--edge-hi);
}
.app-tab {
  font: inherit; font-size: var(--fs-sm); font-weight: 500; cursor: pointer;
  background: transparent; color: var(--text-dim);
  border: 1px solid transparent; border-radius: 999px;
  padding: 8px 16px; min-height: 36px;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.app-tab:hover { color: var(--text); background: var(--glass-strong); }
.app-tab.is-on {
  color: #1a0b02; background: var(--accent); border-color: var(--accent);
  font-weight: 600; box-shadow: var(--shadow-sm);
}
.app-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Copy ABOVE the shot, not beside it. Side-by-side left the screenshot ~635px
   wide for a 1700px capture — every label in the app was illegible, which
   defeats the point of showing the real UI. Stacking gives the shot the full
   container and costs one band of height. */
.app-tour-copy {
  display: grid; grid-template-columns: 1.1fr 1fr; gap: 20px 52px;
  align-items: start; margin-bottom: 26px;
}
.app-tour-copy h3 { font-size: var(--fs-xl); margin-bottom: var(--s2); }
.app-tour-copy > p { color: var(--text-dim); font-size: var(--fs-base); }
.app-tour-copy ul { list-style: none; display: grid; gap: 9px; align-content: center; }
.app-tour-copy li {
  position: relative; padding-left: 18px;
  color: var(--text-dim); font-size: var(--fs-sm); line-height: 1.6;
}
.app-tour-copy li::before {
  content: ""; position: absolute; left: 0; top: 7px;
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 55%, transparent);
}
/* Full-res is a click away — even at full width these are downscaled ~20%. */
.app-shot-link { display: block; cursor: zoom-in; }
.app-tour-zoom {
  display: block; text-align: right; margin-top: var(--s2);
  font-size: var(--fs-xs); color: var(--text-faint);
}
@media (max-width: 900px) {
  .app-tour-copy { grid-template-columns: 1fr; gap: 16px; }
}

/* ── platform availability note ── */
.platform-note {
  max-width: 760px; margin: 44px auto 0; text-align: center;
  font-size: 15px; color: var(--text-dim); line-height: 1.7;
}
.platform-note strong { color: var(--text); font-weight: 600; }
.platform-note .asterisk { color: var(--accent); }
.platform-fineprint { display: block; margin-top: var(--s2); font-size: var(--fs-xs); color: var(--text-faint); }

/* ═══════════ PRIVACY ═══════════ */
.privacy-grid .use-card h3 { display: flex; gap: 10px; align-items: center; }
.privacy .section-head h2 { max-width: 900px; margin: 0 auto; }

@media (max-width: 900px) {
}

/* ═══════════ STORE ═══════════ */
/* Real screenshot of the app's Store tab. The capture ends mid-row, which is
   honest — there are more tiles below the fold — so a mask fades the cut out
   rather than pretending the grid stops there. */
.store-shot {
  width: min(1100px, 100%); margin: 0 auto;
  border: 1px solid var(--line); border-top-left-radius: 16px; border-top-right-radius: 16px;
  overflow: hidden; box-shadow: var(--shadow-lg), var(--edge-hi);
  -webkit-mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
}
.store-shot img { display: block; width: 100%; height: auto; }

/* ═══════════ SDK ═══════════ */
/* ── code ⇄ preview demo ── */
.sdk-demo { max-width: 1100px; margin: 0 auto; }
/* Same segmented control as the app tour — two tab strips on one page that
   look like different components is exactly the drift a design system
   exists to stop. */
.sdk-tabs {
  display: flex; gap: var(--s1); flex-wrap: wrap; justify-content: center;
  margin: 0 auto var(--s5); width: fit-content; max-width: 100%;
  padding: 5px; background: var(--glass);
  backdrop-filter: var(--blur);
  border: 1px solid var(--glass-line); border-radius: 999px;
  box-shadow: var(--edge-hi);
}
.sdk-tab {
  font: inherit; font-size: var(--fs-sm); font-weight: 500; cursor: pointer;
  background: transparent; color: var(--text-dim);
  border: 1px solid transparent; border-radius: 999px;
  padding: 8px 16px; min-height: 36px;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.sdk-tab:hover { color: var(--text); background: var(--glass-strong); }
.sdk-tab.is-on { color: #1a0b02; background: var(--accent); border-color: var(--accent); font-weight: 600; box-shadow: var(--shadow-sm); }
.sdk-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.sdk-panes { display: grid; grid-template-columns: 1fr 480px; gap: 32px; align-items: start; justify-content: center; }
/* Fixed height, not content height: the code pane must not resize the row when
   one example runs longer than another, or the preview beside it would jump.
   366px is the device frame's rendered height, so the two blocks match. */
.sdk-code { position: relative; }
.sdk-code pre,
.sdk-code textarea {
  background: var(--bg-sunken); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: 22px 24px; width: 100%; height: 366px; overflow: auto;
  font-family: var(--mono); font-size: 12px; line-height: 1.65;
  tab-size: 2; box-shadow: var(--edge-hi);
}
/* ── live editor ──
   The page's core claim is "a tile is plain HTML you write yourself", and
   until now the demo was read-only — you could admire the claim but not test
   it. Editing swaps the highlighted <pre> for a textarea over the same
   source string and re-renders the sandboxed frame as you type. Highlighting
   is dropped while editing on purpose: keeping a caret stable inside a
   re-highlighted contenteditable is a bug farm, and a plain textarea is
   the thing that always works. */
.sdk-code textarea {
  display: block; color: #c9d3e0; resize: none;
  border-color: var(--accent-line); outline: none;
  caret-color: var(--accent); white-space: pre;
}
.sdk-code textarea:focus { border-color: var(--accent); }
.sdk-edit-bar {
  position: absolute; top: 10px; right: 12px; z-index: 2;
  display: flex; gap: 6px;
}
.sdk-edit-btn {
  font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
  padding: 5px 11px; min-height: 28px; border-radius: 999px;
  background: rgba(19, 22, 28, .86); backdrop-filter: blur(8px);
  border: 1px solid var(--line); color: var(--text-dim);
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}
.sdk-edit-btn:hover { color: var(--text); border-color: var(--line-strong); }
.sdk-edit-btn.is-on { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
.sdk-live {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; color: var(--text-faint);
}
.sdk-live::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--green); box-shadow: 0 0 0 3px rgba(62, 207, 142, .16);
}
.sdk-preview { margin: 0; }
.sdk-preview .device-frame { width: 100%; animation: none; }
/* The iframe is authored at 480×320 and scaled by JS; the stage just clips it. */
.sdk-stage { width: 100%; }
.sdk-stage iframe {
  display: block; border: 0; width: 480px; height: 320px;
  transform-origin: top left;
}
.sdk-preview figcaption {
  margin-top: var(--s3); text-align: center;
  font-size: var(--fs-xs); color: var(--text-faint);
}

@media (max-width: 900px) {
  .sdk-panes { grid-template-columns: 1fr; }
}
.sdk-code .c { color: #667381; }
.sdk-code .t { color: #7ee787; }
.sdk-code .a { color: #79c0ff; }
.sdk-code .s { color: #a5d6ff; }
.sdk-code .k { color: #ff7b72; }
.sdk-code .f { color: #d2a8ff; }
.sdk-code .n { color: #79c0ff; }

/* ═══════════ SIGNUP ═══════════ */
.signup { text-align: center; }
.signup-inner h2 { margin-bottom: var(--s4); }
.price { color: var(--accent); }
.signup-inner p { color: var(--text-dim); font-size: var(--fs-md); max-width: 58ch; margin: 0 auto var(--s7); }
.signup-form { display: flex; gap: var(--s3); justify-content: center; flex-wrap: wrap; }
.signup-form input[type=email] {
  background: var(--glass); border: 1px solid var(--glass-line); border-radius: 999px;
  padding: 13px 22px; min-height: 46px;
  font: inherit; font-size: var(--fs-base); color: var(--text);
  width: min(340px, 100%);
  outline: none; box-shadow: var(--edge-hi);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.signup-form input[type=email]::placeholder { color: var(--text-faint); }
/* A focus ring, not just a recoloured hairline — the field is the one thing
   on this page someone has to type into. */
.signup-form input[type=email]:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft), var(--edge-hi);
}
.signup-done { color: var(--green); font-weight: 600; margin-top: var(--s5); }
.signup-error { color: #ff6b6b; font-weight: 600; margin-top: var(--s4); }
/* Honeypot: a real field, visually gone, never announced, never tabbable.
   `display:none` is skipped by some bots; this is not. */
.hp-field {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}
.signup-form button[disabled] { opacity: .6; cursor: not-allowed; }
.footer {
  margin-top: var(--s12); padding-top: var(--s7); border-top: 1px solid var(--line-soft);
  color: var(--text-faint); font-size: var(--fs-sm);
}
.footer-row { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
/* Legal fine print: present and readable, but never competing for attention.
   Opacity floors at .78 — anything lighter drops this 11px text under the
   4.5:1 contrast minimum, and unreadable small print is the kind a court
   treats as not there at all. */
.footer-legal {
  margin-top: 14px; max-width: 70ch;
  font-size: 11.5px; line-height: 1.55; color: var(--text-faint);
}

/* ═══════════ CINEMATIC MOTION ═══════════
   Scroll-driven animation, native. `animation-timeline: view()` drives these
   from the element's own progress through the viewport — no scroll handler,
   no rAF loop, no jank, and it runs off the main thread. Everything here is
   inside @supports: browsers without it get the static layout, which is the
   whole page working exactly as it did. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* Hero device: drifts up and settles as the hero leaves. Paired with the
       copy sliding the other way, this is the parallax that gives the fold
       depth instead of one flat plane. */
    .hero-device {
      animation: dolly linear both;
      animation-timeline: view();
      animation-range: exit -10% exit 90%;
    }
    @keyframes dolly {
      to { transform: translateY(-46px) scale(.94); opacity: .55; }
    }

    .hero-copy {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 85%;
    }
    @keyframes drift {
      to { transform: translateY(22px); opacity: .35; }
    }

    /* Big media rises INTO place as it enters — the classic product-page
       move. Range stops at cover 45% so it is fully settled well before it
       reaches the middle of the screen. */
    /* NOT .wt-screen: the carousel toggles slide visibility, and a
       fill-mode animation on a slide that was never in view would hold it at
       opacity .35 the moment it became the active one. */
    .app-shot, .store-shot, .voice-shot img {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 42%;
    }
    @keyframes rise-in {
      from { transform: translateY(46px) scale(.965); opacity: .35; }
      to { transform: none; opacity: 1; }
    }

  }
}

/* ── the spotlight ──
   A soft pool of light behind each section head. Cheap depth: it separates
   the band from the one above it without a divider, and it is what makes the
   backdrop read as lit from somewhere rather than evenly tinted. */
.section-head { position: relative; }
.section-head::before {
  content: ""; position: absolute; z-index: -1;
  left: 50%; top: -30%; translate: -50% 0;
  width: min(880px, 92vw); height: 320px;
  background: radial-gradient(50% 50% at 50% 50%, rgba(255, 255, 255, .045), transparent 70%);
  pointer-events: none;
}

/* ── full-bleed media ──
   These are 1300–1700px captures of a dense desktop app. Gutters buy nothing
   and cost the only detail that makes the shot worth showing, so the big
   media breaks the container and fades at the edges instead of ending on a
   hard rectangle. */
.app-tour, .store-shot {
  width: min(1440px, 96vw);
  margin-inline: auto;
  max-width: none;
}
/* No fade on .app-shot: it is a bordered panel whose whole job is showing
   legible detail, and a mask would eat both the border and the bottom row of
   the UI. .store-shot keeps the fade it already had — that capture genuinely
   ends mid-row, so fading it is honest rather than decorative. */

/* ═══════════ RESPONSIVE ═══════════
   Three tiers, not one. The sheet used to break only at 900px, so a 375px
   phone got the same type sizes, card padding and grid gaps as a 899px
   tablet — which is why everything below the hero felt cramped rather than
   compact. */

/* ── tablet / narrow laptop ── */
@media (max-width: 900px) {
  .hero { min-height: 0; padding-top: 108px; }
  .hero-sticky, .exploded, .wt { grid-template-columns: 1fr; gap: var(--s8); }
  /* Device first on a phone: it is the product shot, and a headline above
     the fold with nothing to look at beside it wastes the only screen most
     visitors will see. */
  .hero-device { order: -1; }
  .wt-flip .wt-copy { order: 0; }
  .layer-tag { right: 8px; }

  /* ── the mobile menu takes over ── */
  .nav-toggle { display: inline-flex; }
  .nav-links {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    padding: var(--s2) var(--gutter) var(--s5);
    /* Fully opaque, not a translucent sheet. backdrop-filter is what would
       have made a translucent panel legible, and it is exactly the thing
       that silently does nothing on a low-power or GPU-blocked device —
       leaving the hero showing through the menu text. */
    background: #0b0d12;
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-lg);
    font-size: var(--fs-md);
    /* Closed state is inert, not merely invisible: no tab stops, no
       screen-reader announcements, no click targets floating off-screen. */
    visibility: hidden; opacity: 0; transform: translateY(-8px);
    pointer-events: none;
    transition: opacity var(--t) var(--ease), transform var(--t) var(--ease-out),
                visibility var(--t);
  }
  .nav-links.is-open {
    visibility: visible; opacity: 1; transform: none; pointer-events: auto;
  }
  .nav-links a {
    padding: 14px 4px; border-bottom: 1px solid var(--line-soft);
    min-height: 48px; display: flex; align-items: center;
  }
  .nav-links a.is-current::after { display: none; }
  .nav-links a.is-current { color: var(--accent); }
  .nav-links .nav-cta {
    margin: var(--s4) 0 0; justify-content: center;
    padding: 14px 16px !important; border-bottom: 0;
  }
}

/* ── phone ── */
@media (max-width: 600px) {
  :root {
    --section-y: clamp(64px, 14vw, 88px);
    --radius-xl: 20px;
  }
  .hero { padding-top: 96px; }
  .hero-copy p { max-width: none; }
  .hero-actions { gap: var(--s2); }
  .hero-actions .btn { flex: 1 1 100%; }
  .use-card, .lane { padding: var(--s6) var(--s5); }
  .wt { padding: var(--s5); }
  .wt-thumbs .tilecard { width: calc(50% - var(--s2)); }
  .section-head { margin-bottom: var(--s8); }
  .faq-list details { padding: 0 var(--s5); }
  .tilecard { width: 264px; }
  /* Full-bleed screenshots: a 20px gutter each side of a 1700px capture buys
     nothing and costs the only detail that makes the shot worth showing. */
  .store-shot, .app-shot { border-radius: var(--radius); }
  .footer-row { justify-content: center; text-align: center; }
}

/* ── coarse pointers ──
   Hover states that persist after a tap are the classic touch bug: the card
   you pressed stays lifted and highlighted until you tap something else. */
@media (hover: none) {
  .use-card:hover, .lane:hover, .tilecard:hover, .btn:hover, .wt-arrow:hover {
    transform: none; box-shadow: var(--edge-hi);
  }
  .use-card:hover::before { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .marquee-track, .hero-device, .m-wicon { animation: none !important; }
  .reveal { opacity: 1; transform: none; transition: none; }
  /* Belt and braces: nothing on the page may run longer than an instant. */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ═══════════ additions from the site review (2026-08-07) ═══════════ */

/* ── platform chip ──
   The macOS requirement, as one pill in the "Compatible with" strip rather
   than a sentence under the buttons. Marked out from the integration chips
   by a dotted underline on the label: it is the one entry that is a
   requirement rather than something the product talks to, and it links to
   the FAQ answer that covers Windows. */
.works-with .wm-platform a { display: block; }
.works-with .wm-platform .wm-name {
  color: var(--text-dim);
  border-bottom: 1px dotted var(--line-strong);
  padding-bottom: 1px;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.works-with .wm-platform:hover .wm-name { color: var(--text); border-bottom-color: var(--accent); }

/* ── who it's for ── */
/* ── who it's for: three tiers, not two whites ──
   These cards had a white bold <h3> sitting directly on top of a white bold
   <em>, so two elements were both claiming to be the emphasis inside three
   lines of copy. The fix is not to recolour the heading — it is to notice
   that "The developer" is a CATEGORY and "The build goes red at 4:51pm" is
   the actual hook. So the heading demotes to a quiet label and the scenario
   becomes the headline it always was.

   No accent here on purpose: the section kicker above is already accent, and
   an accent label inside every card would compete with it and spend the
   accent on something that is not a call to action. */
.usecases .use-card h3 {
  font-size: var(--fs-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-faint);
  margin-bottom: var(--s4);
}
.usecases .use-card em {
  display: block;
  color: var(--text); font-style: normal;
  font-weight: 700; font-size: var(--fs-md); line-height: 1.35;
  letter-spacing: var(--track-head);
  margin-bottom: var(--s3);
}

/* ── compare table ──
   Wrapped in its own scroll container: the page body must never scroll
   sideways on a phone just because a table is wider than the viewport. */
.compare-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.compare-table {
  width: 100%; min-width: 720px; border-collapse: collapse;
  background: var(--glass); border: 1px solid var(--glass-line); border-radius: var(--radius-lg);
  overflow: hidden; font-size: var(--fs-base);
}
.compare-table th, .compare-table td {
  padding: 15px 18px; text-align: left; border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.compare-table thead th {
  font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-faint); background: rgba(255, 255, 255, .04); font-weight: 600;
}
.compare-table thead th:nth-child(2) { color: var(--accent); }
.compare-table tbody th { font-weight: 500; color: var(--text); width: 32%; }
.compare-table td { color: var(--text-dim); }
.compare-table td.yes { color: var(--text); font-weight: 500; }
.compare-table td.yes::before { content: "✓ "; color: var(--green); font-weight: 700; }
.compare-table tbody tr:last-child th, .compare-table tbody tr:last-child td { border-bottom: 0; }
.compare-table tbody tr { transition: background var(--t-fast) var(--ease); }
.compare-table tbody tr:hover th, .compare-table tbody tr:hover td { background: rgba(255, 255, 255, .035); }

/* ── status & roadmap ── */
.status-cols {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 24px; max-width: 1080px; margin: 0 auto;
}
.status-col {
  background: var(--glass); border: 1px solid var(--glass-line);
  border-radius: var(--radius-lg); padding: var(--s7) var(--s7);
  box-shadow: var(--edge-hi);
}
.status-col h3 { font-size: var(--fs-h3); margin-bottom: var(--s5); padding-bottom: var(--s4); border-bottom: 1px solid var(--line-soft); }
.status-list { list-style: none; display: grid; gap: 14px; }
.status-list li { position: relative; padding-left: 28px; color: var(--text-dim); font-size: var(--fs-base); line-height: 1.6; }
.status-list li::before {
  position: absolute; left: 0; top: 0; font-weight: 700;
}
.status-done li::before { content: "✓"; color: var(--green); }
/* Open, not failed — an amber arrow, never a red cross. */
.status-todo li::before { content: "→"; color: var(--accent); }

/* ── FAQ ──
   <details> rather than JS: it is keyboard- and screen-reader-correct for
   free, and it still works with scripting off. */
.faq-list { max-width: 820px; margin: 0 auto; display: grid; gap: var(--s2); }
.faq-list details {
  background: var(--glass); border: 1px solid var(--glass-line);
  border-radius: var(--radius-lg); padding: 0 var(--s6);
  transition: border-color var(--t) var(--ease), background var(--t) var(--ease);
}
.faq-list details:hover { border-color: var(--glass-line-hi); }
.faq-list details[open] { border-color: var(--accent-line); background: var(--glass-strong); }
.faq-list summary {
  cursor: pointer; padding: 18px 0; font-weight: 600; font-size: var(--fs-md);
  letter-spacing: var(--track-body);
  list-style: none; display: flex; justify-content: space-between; gap: var(--s4);
  align-items: center; min-height: 56px;
}
.faq-list summary::-webkit-details-marker { display: none; }
.faq-list summary::after {
  content: "+"; color: var(--accent); font-size: 22px; font-weight: 400;
  line-height: 1; transition: transform var(--t) var(--ease-out); flex: none;
}
.faq-list details[open] summary::after { transform: rotate(45deg); }
.faq-list summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.faq-list details p {
  color: var(--text-dim); padding: 0 0 20px; font-size: var(--fs-base); line-height: 1.7;
  max-width: 66ch;
}
/* Height animation for <details>, no JS. `interpolate-size` lets `height`
   transition to and from `auto`; browsers without it simply snap open, which
   is the behaviour we had anyway. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .faq-list details::details-content {
    height: 0; overflow: hidden; opacity: 0;
    transition: height var(--t) var(--ease-out), opacity var(--t) var(--ease),
                content-visibility var(--t) allow-discrete;
  }
  .faq-list details[open]::details-content { height: auto; opacity: 1; }
}

/* ── waitlist: segmentation + expectation-setting ── */
/* Legend on its own centred line: floating it beside the radios makes the
   labels wrap mid-phrase at narrow widths. */
.signup-seg {
  border: 0; flex-basis: 100%; margin-top: 14px;
  text-align: center; font-size: 14px; color: var(--text-dim);
}
.signup-seg legend { width: 100%; padding: 0 0 8px; text-align: center; }
.signup-seg label {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 0 10px; cursor: pointer; white-space: nowrap;
}
.signup-seg input { accent-color: var(--accent); }
.signup-fine { font-size: var(--fs-sm); color: var(--text-faint); margin-top: var(--s5); }

/* ── footer ── */
.footer-links {
  display: flex; flex-wrap: wrap; gap: 10px 26px; justify-content: center;
  padding-bottom: 22px; font-size: 14px;
}
.footer-links a { color: var(--text-dim); transition: color var(--t-fast) var(--ease); }
.footer-links a:hover { color: var(--accent); }

/* Social row. Tap targets are 44px so a thumb can hit them on mobile, while the
   glyph itself stays 20px — the padding does the work, not the icon. */
.footer-social {
  display: flex; gap: 8px; justify-content: center; padding-bottom: 26px;
}
.footer-social a {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 10px;
  color: var(--text-dim); transition: color .15s ease, background .15s ease;
}
.footer-social a:hover { color: var(--accent); background: rgba(255,255,255,.05); }
.footer-social a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }


/* ── voice: real device frames under the lanes ──
   Deliberately not the hero's .device-frame: that one is a fixed-width prop
   for a single hero shot, and two of them side by side overflow the column. */
.voice-shots {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 30px; margin-top: 44px; max-width: 940px; margin-left: auto; margin-right: auto;
}
.voice-shot { margin: 0; }
.voice-shot img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-sunken);
  box-shadow: var(--shadow-md);
}
.voice-shot figcaption {
  color: var(--text-faint); font-size: var(--fs-sm); line-height: 1.6;
  text-align: center; margin-top: var(--s3);
}
.voice-shot figcaption em { color: var(--text); font-style: normal; }
