/* ==========================================================================
   TextLoupe — textloupe.com
   Plain CSS. No build step, no framework, no webfont.

   The palette is not invented: every colour under "product surfaces" is read
   straight out of the `B` enum at the top of mac/Sources/TextLoupe/PopoverView.swift,
   so the panel in the demo is the same charcoal and the same violet as the panel
   the customer will actually see. Change one and change the other.

   Radius system, applied everywhere:
       --r-lg 12px  surfaces and cards        (the app's panel radius)
       --r-md 10px  buttons and inputs
       --r-sm  7px  chips and small controls  (the app's chip radius)
     The one exception is the demo's text box at 8px, which is the app's own
     field radius; the facsimile follows the app rather than the page.
   ========================================================================== */

/* ---------------------------------------------------------------- tokens */

:root {
  color-scheme: dark;

  /* Page surfaces */
  --bg:         #131316;
  --surface:    #1A1A1F;
  --line:       #26262C;
  --line-2:     #33333A;

  /* Page text. --t3 is deliberately lighter than the app's own tertiary
     (#787880), which sits at 4.2:1 on this background and fails AA for body
     copy. The app's value survives only inside the demo, as product chrome. */
  --t1:         #E8E8EA;
  --t2:         #A1A1A8;
  --t3:         #8A8A93;

  /* Product surfaces — verbatim from PopoverView.swift's `B` */
  --pop-panel:        #1E1E22;
  --pop-panel-border: #38383E;
  --pop-field:        #161619;
  --pop-field-border: #2E2E33;
  --pop-t1:           #E8E8EA;
  --pop-t2:           #A1A1A8;
  --pop-t3:           #787880;
  --pop-chip:         #2A2A30;
  --pop-chip-hover:   #36363D;
  --pop-chip-text:    #D8D8DC;

  /* The one accent. Nothing else on this page is coloured. */
  --accent:       #7F77DD;
  --accent-hover: #8F87E6;
  --on-accent:    #16161A;

  --r-lg: 12px;
  --r-md: 10px;
  --r-sm: 7px;

  /* Motion. The built-in CSS easings are too weak to read as intentional;
     these are the stronger variants. Nothing here uses ease-in: it delays the
     first frame, which is the frame the eye is on. */
  --ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --d-press:  140ms;
  --d-fast:   180ms;
  --d-ui:     240ms;
  --d-reveal: 260ms;

  --wrap: 1120px;
  --gutter: clamp(1.25rem, 5vw, 2.5rem);

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
          system-ui, Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* ------------------------------------------------------------------ base */

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--t1);
  font: 400 1rem/1.6 var(--font);
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}
/* `clip` and not `hidden`: hidden makes the element a scroll container, which
   would break both the sticky nav above it and the pinned demo below. clip
   trims the same overflow without creating one. The fallback is only reached
   on browsers that predate `clip`, which also predate the pin. */
@supports not (overflow: clip) {
  body { overflow-x: hidden; }
}

/* Lenis, when it is running, owns the scroll position; the CSS keyword and the
   library would otherwise both animate the same jump to an anchor. Lenis puts
   the `lenis` class on <html> itself, so this only applies while it is live.
   Reproduced from the library's own lenis.css rather than loading it: the two
   rules this page can reach are not worth a second stylesheet request. */
html.lenis, html.lenis body { height: auto; }
html.lenis { scroll-behavior: auto; }
html.lenis.lenis-stopped { overflow: clip; }

/* Every anchor target clears the 64px bar, with room to breathe. Read by
   Lenis's scrollTo as well as by the browser's own anchor jump, so the two
   agree on where a section starts. */
#what-it-does, #pricing, #faq { scroll-margin-top: 88px; }

img { max-width: 100%; height: auto; display: block; }

a { color: inherit; }

h1, h2, h3 { margin: 0; font-weight: 600; }

p { margin: 0; }

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

::selection { background: rgba(127, 119, 221, 0.32); color: #fff; }

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

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

.skip {
  position: absolute; left: 50%; top: 0;
  transform: translate(-50%, -120%);
  z-index: 100;
  background: var(--accent); color: var(--on-accent);
  padding: 0.6rem 1rem; border-radius: 0 0 var(--r-md) var(--r-md);
  font-size: 0.875rem; font-weight: 600; text-decoration: none;
}
.skip:focus { transform: translate(-50%, 0); }

#top-sentinel { position: absolute; top: 0; height: 1px; width: 1px; }

/* -------------------------------------------------------------- utility */

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

/* Type. Tracking is size-specific: large text needs it negative or the letters
   read too far apart as they grow; body copy sits at zero. */
.h2 {
  font-size: clamp(1.75rem, 4.2vw, 2.75rem);
  line-height: 1.1;
  letter-spacing: -0.025em;
}
.h2--center { text-align: center; }

.h3 {
  font-size: 1.0625rem;
  line-height: 1.3;
  letter-spacing: -0.011em;
}

.lede {
  margin-top: 1rem;
  color: var(--t2);
  font-size: clamp(1rem, 2vw, 1.125rem);
  line-height: 1.65;
  text-align: center;
  max-width: 46ch;
  margin-inline: auto;
}

.ico {
  width: 1em; height: 1em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ico--xs { stroke-width: 2.6; }

kbd.k, .cap {
  font-family: var(--font);
  font-size: 0.9em;
  background: var(--pop-chip);
  border: 1px solid var(--line-2);
  border-radius: 5px;
  padding: 0.05em 0.4em;
  margin-inline: 0.06em;
  color: var(--t1);
}

/* ------------------------------------------------------------- buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font: 600 0.9375rem/1 var(--font);
  letter-spacing: -0.008em;
  text-decoration: none;
  padding: 0.75rem 1.15rem;
  cursor: pointer;
  transition: background-color var(--d-fast) ease,
              border-color var(--d-fast) ease,
              transform var(--d-press) var(--ease-out);
}
/* Instant, physical feedback on press. Scale, not colour: colour alone reads
   as a state change, scale reads as the surface moving under the finger. */
.btn:active { transform: scale(0.97); }

.btn--primary { background: var(--accent); color: var(--on-accent); }
.btn--ghost {
  background: transparent;
  color: var(--t1);
  border-color: var(--line-2);
}
.btn--sm { padding: 0.5rem 0.85rem; font-size: 0.875rem; }
.btn--lg { padding: 0.9rem 1.5rem; font-size: 1rem; }
.btn--block { width: 100%; }
.btn .ico { width: 1.1em; height: 1.1em; }

@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover { background: var(--accent-hover); }
  .btn--ghost:hover { border-color: var(--accent); }
}

/* ----------------------------------------------------------------- nav */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(19, 19, 22, 0.72);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border-bottom: 1px solid transparent;
  transition: border-color var(--d-ui) ease, background-color var(--d-ui) ease;
}
/* The hairline only exists once something has scrolled under the bar. Chrome
   that floats over content should show its edge only where it overlaps. */
.nav.is-stuck {
  border-bottom-color: var(--line);
  background: rgba(19, 19, 22, 0.86);
}
/* Past the hero the bar is over content for the rest of the page, so it stops
   pretending to be part of it: a little more tint, a little more blur. Two
   values, no movement — a bar that resizes as you scroll is a bar that makes
   the page under it jump. */
.nav.is-deep {
  background: rgba(19, 19, 22, 0.94);
  backdrop-filter: blur(28px) saturate(180%);
  -webkit-backdrop-filter: blur(28px) saturate(180%);
}

.nav__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  padding: 0 var(--gutter);
  height: 64px;
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  text-decoration: none;
  font-weight: 600;
  letter-spacing: -0.02em;
  font-size: 1.0625rem;
}
.brand__icon { border-radius: 6px; }

.nav__links {
  margin-left: auto;
  display: flex;
  gap: 1.5rem;
  font-size: 0.9375rem;
}
.nav__links a {
  color: var(--t2);
  text-decoration: none;
  transition: color var(--d-fast) ease;
  white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .nav__links a:hover { color: var(--t1); }
}

@media (max-width: 640px) {
  .nav__links { display: none; }
  .nav__inner { gap: 1rem; }
  .brand { margin-right: auto; }
}

/* ---------------------------------------------------------------- hero */

.hero {
  padding: clamp(3rem, 8vw, 6rem) 0 clamp(2.5rem, 6vw, 4rem);
}

.hero__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(2rem, 6vw, 4rem);
  align-items: center;
}

.hero__title {
  font-size: clamp(2.5rem, 7.2vw, 4.5rem);
  line-height: 1.03;
  letter-spacing: -0.035em;
  max-width: 12ch;
}

.hero__sub {
  margin-top: 1.4rem;
  max-width: 44ch;
  color: var(--t2);
  font-size: clamp(1.0625rem, 2.2vw, 1.25rem);
  line-height: 1.55;
}

.hero__cta { margin-top: 2rem; }

.hero__meta {
  margin-top: 1rem;
  color: var(--t3);
  font-size: 0.875rem;
}

.hero__art {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 1.75rem;
}

.hero__glow {
  position: absolute;
  inset: -18% -10% 10%;
  background: radial-gradient(closest-side, rgba(127, 119, 221, 0.24), transparent 78%);
  pointer-events: none;
}

.hero__icon {
  position: relative;
  width: clamp(120px, 18vw, 160px);
  height: auto;
}

.hero__keys { position: relative; display: flex; gap: 0.5rem; }

.cap {
  display: grid;
  place-items: center;
  min-width: 42px;
  height: 42px;
  padding: 0;
  font-size: 1rem;
  font-weight: 600;
  border-radius: var(--r-md);
  background: var(--pop-chip);
  border: 1px solid var(--line-2);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

@media (max-width: 860px) {
  .hero__inner { grid-template-columns: 1fr; gap: 2rem; }
  .hero__title { max-width: 16ch; }
  .hero__art { order: -1; justify-items: start; }
  .hero__glow { inset: -30% 30% 0 -20%; }
  .hero__icon { width: 104px; }
  /* The sub-line already renders the hotkey inline a few centimetres below, and
     on one column the second copy only pushes the headline and the button
     further down a viewport that has none to spare. */
  .hero__keys { display: none; }
}

/* ==========================================================================
   The demo

   A re-enactment of the real interaction, built out of DOM and CSS so the text
   stays crisp at any zoom and the whole thing costs a few kilobytes instead of
   a video. Everything is driven by classes on #stage, set by main.js; the CSS
   below only describes what each class means. Nothing animates except transform,
   opacity, filter and clip-path.
   ========================================================================== */

.demo { padding: clamp(2rem, 6vw, 4rem) 0 clamp(3.5rem, 9vw, 6rem); }
.demo .h2 { margin-bottom: clamp(2rem, 5vw, 3rem); }

.stage {
  position: relative;
  /* Reserved so the absolutely-positioned panel cannot shift the page. The
     tallest thing in here is the panel, whose bottom edge measures 350px from
     the top of the stage; the rest is headroom. */
  min-height: 376px;
  max-width: 940px;
  margin-inline: auto;
}

/* ------------------------------------------------------- the document */

.doc {
  width: min(560px, 100%);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}

.doc__bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 0.85rem;
  border-bottom: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.015);
}

/* Grey, not red/amber/green: while the TextLoupe panel is up this window is in
   the background, and that is exactly how macOS draws an unfocused window. It
   also keeps the page to a single accent. */
.doc__lights { display: flex; gap: 6px; }
.doc__lights i {
  width: 11px; height: 11px;
  border-radius: 50%;
  background: #3A3A42;
}

.doc__title {
  font-size: 0.8125rem;
  color: var(--t3);
  letter-spacing: -0.005em;
}

.doc__body { padding: 1.15rem 1.25rem 1.5rem; }

.doc__to {
  font-size: 0.8125rem;
  color: var(--t3);
  padding-bottom: 0.85rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--line);
}
.doc__to span { color: #6A6A73; margin-right: 0.4rem; }

.doc__p { font-size: 0.9375rem; line-height: 1.75; }
.doc__p--dim { color: var(--t3); }

/* Both wordings occupy the same grid cell, so swapping them cannot reflow. */
.doc__sent {
  display: grid;
  margin: 0.5rem 0;
  font-size: 0.9375rem;
  line-height: 1.75;
}
.doc__layer {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* One span per line, at max-content width, so the highlight behind it hugs the
   text exactly instead of painting a ragged last line full width. The breaks are
   authored rather than left to wrapping, which is what makes the sweep
   deterministic at every viewport. */
.ln {
  position: relative;
  z-index: 0;
  width: max-content;
  max-width: 100%;
  white-space: pre-wrap;
}

.hl {
  position: absolute;
  inset: -1px -3px;
  z-index: -1;
  border-radius: 3px;
  background: rgba(127, 119, 221, 0.30);
  opacity: 0;
  /* Revealed left to right, like a real drag-select. clip-path is free: no
     layout, no paint of anything underneath. */
  clip-path: inset(0 100% 0 0);
  transition: opacity 220ms ease;
}
.is-sweep .hl {
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--t) var(--ease-out) var(--d), opacity 120ms ease;
}
.is-sel .hl { opacity: 1; }

/* The correction crossfades in under a light blur. Without it the eye reads two
   sentences overlapping; the blur bridges them into one change. */
.doc__layer--before {
  opacity: 1;
  filter: blur(0);
  transition: opacity 240ms ease, filter 240ms ease;
}
.doc__layer--after {
  opacity: 0;
  filter: blur(3px);
  transition: opacity 240ms ease 60ms, filter 240ms ease 60ms;
}
.is-swapped .doc__layer--before { opacity: 0; filter: blur(3px); }
.is-swapped .doc__layer--after  { opacity: 1; filter: blur(0); }

/* ---------------------------------------------------------- the panel */

.pop {
  position: absolute;
  left: min(300px, 42%);
  top: 208px;
  width: min(452px, 100%);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;

  background: rgba(30, 30, 34, 0.86);
  border: 1px solid var(--pop-panel-border);
  border-radius: var(--r-lg);
  box-shadow: 0 22px 60px rgba(0, 0, 0, 0.55);

  opacity: 0;
  transform: translateY(-6px) scale(0.96);
  /* Scaling from the top-left corner points the motion back at the selection
     it belongs to, rather than out of the middle of nowhere. */
  transform-origin: 14% 0;
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  /* Closed state carries the exit timing: the browser uses the transition of
     whichever state it is moving *to*, so the panel leaves faster than it
     arrives, which is what a dismissal should feel like. */
  transition: opacity 190ms var(--ease-out),
              transform 190ms var(--ease-out),
              backdrop-filter 190ms var(--ease-out);
}
.is-open .pop {
  opacity: 1;
  transform: translateY(0) scale(1);
  /* Blur and scale arrive together so the surface reads as a material landing,
     not a flat rectangle fading up. */
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  transition: opacity var(--d-reveal) var(--ease-out),
              transform var(--d-reveal) var(--ease-out),
              backdrop-filter var(--d-reveal) var(--ease-out);
}

/* --- text box --- */

.pop__field {
  position: relative;
  display: grid;
  min-height: 76px;
  padding: 10px 11px;
  border-radius: 8px;
  background: var(--pop-field);
  border: 1px solid var(--pop-field-border);
  overflow: hidden;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--pop-t1);
  transition: opacity 200ms ease;
}
/* The app dims the text while a call is running and leaves it in place. */
.is-busy .pop__field { opacity: 0.55; }

.pop__layer {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
/* The result layer is switched on with no fade at all, because at that instant
   it is still clipped to nothing and there is nothing to see anyway; the
   original clears out from under it over the first frames of typing. Crossfading
   two blocks of text through each other just renders both at half strength and
   reads as a printing error. */
.pop__layer--in  { opacity: 1; transition: opacity 90ms linear; }
.pop__layer--out { opacity: 0; transition: opacity 1ms linear; }
.is-result .pop__layer--in  { opacity: 0; }
.is-result .pop__layer--out { opacity: 1; }

.pop__shimmer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(100deg,
              transparent 25%,
              rgba(127, 119, 221, 0.14) 50%,
              transparent 75%);
  transform: translateX(-100%);
}
.is-busy .pop__shimmer {
  opacity: 1;
  animation: shimmer 1150ms linear infinite;
}
@keyframes shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* --- the typewriter ---
   The finished line is already in the DOM and simply uncovered left to right.
   steps() quantises the reveal to one character per step, which is what makes it
   read as typing rather than as a wipe; the caret is a full-width track whose
   right edge carries the bar, translated on the same steps, so it always sits
   exactly at the reveal edge. Both are transforms and clips: no reflow, no
   text mutation, nothing for the layout engine to do. */
.ty { position: relative; }

.ty__t {
  display: inline-block;
  clip-path: inset(0 100% 0 0);
}
.is-typed .ty__t { clip-path: inset(0 0 0 0); }

.ty__c {
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  opacity: 0;
  pointer-events: none;
}
.ty__c::after {
  content: "";
  position: absolute;
  right: -1px; top: 6%; bottom: 6%;
  width: 1.5px;
  background: var(--accent);
}
/* Each caret lives only for its own line's window, so line two's bar is not
   parked at the left margin while line one is still typing. `forwards` and not
   `both`, deliberately: with no backwards fill the delayed caret keeps the base
   opacity of 0 until its turn actually comes. */
.is-typing .ty:nth-child(1) .ty__c {
  animation: caret-move 560ms steps(36) forwards,
             caret-show 560ms linear forwards;
}
.is-typing .ty:nth-child(2) .ty__c {
  animation: caret-move 480ms steps(31) 560ms forwards,
             caret-show 480ms linear 560ms forwards;
}
@keyframes caret-move { to { transform: translateX(0); } }
@keyframes caret-show { 0%, 96% { opacity: 1; } 100% { opacity: 0; } }

.ty:nth-child(1) .ty__t { transition: clip-path 560ms steps(36) 0ms; }
.ty:nth-child(2) .ty__t { transition: clip-path 480ms steps(31) 560ms; }

/* At rest the reveal must snap back instantly, or the loop spends half a second
   un-typing itself behind a hidden panel. */
.stage[data-state="idle"] .ty__t { transition: none; }

/* --- action rows ---
   All three rows share one fixed-height box and crossfade, so the panel never
   resizes mid-loop and the page never shifts. */
.pop__rows { position: relative; min-height: 30px; }

.pop__row {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 5px;
  opacity: 0;
  transition: opacity 90ms linear;
}
.pop__row--ready { opacity: 1; }

/* Handed over, not crossfaded. The outgoing row is gone in 90ms and the
   incoming one does not begin until 100ms, so the two sets of labels are never
   on screen together at half opacity, which is unreadable and looks broken. */
.is-busy .pop__row--ready,
.is-result .pop__row--ready { opacity: 0; transition: opacity 90ms linear; }
.is-busy .pop__row--busy { opacity: 1; transition: opacity 150ms var(--ease-out) 100ms; }
.is-done .pop__row--done { opacity: 1; transition: opacity 150ms var(--ease-out) 100ms; }

.pop__gap { flex: 1 1 auto; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 10px;
  border-radius: var(--r-sm);
  background: var(--pop-chip);
  color: var(--pop-chip-text);
  font-size: 0.75rem;
  line-height: 1.25;
  white-space: nowrap;
  transition: background-color var(--d-fast) ease,
              transform var(--d-press) var(--ease-out);
}
.chip .ico { width: 12px; height: 12px; }

.chip--accent {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 500;
}

.chip--split { padding: 0; overflow: hidden; }
.chip__main {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 8px 6px 10px;
}
.chip__rule { width: 1px; align-self: stretch; margin: 5px 0; background: currentColor; opacity: 0.25; }
.chip__caret { display: inline-flex; padding: 6px 7px; }
.chip__caret .ico { width: 10px; height: 10px; }

/* The press. Down is quick and the release is quicker: the user is waiting on
   the release, never on the press. */
.stage[data-press="fix"] #chip-fix,
.stage[data-press="replace"] #chip-replace {
  transform: scale(0.97);
  transition-duration: 110ms;
}
.stage[data-press="fix"] #chip-fix { background: var(--pop-chip-hover); }
.stage[data-press="replace"] #chip-replace { background: var(--accent-hover); }

.pop__globe {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 7px;
  color: var(--pop-t2);
  font-size: 0.6875rem;
  font-weight: 600;
}
.pop__globe .ico { width: 12px; height: 12px; }

.spin {
  width: 13px; height: 13px;
  border-radius: 50%;
  border: 2px solid var(--pop-t3);
  border-top-color: transparent;
  animation: spin 640ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.pop__busy-label { font-size: 0.75rem; color: var(--pop-t2); }

/* Product chrome, verbatim from the app's result footer. */
.pop__usage {
  font-family: var(--mono);
  font-size: 0.625rem;
  color: var(--pop-t3);
}

/* --- demo, small screens ---
   Below 860px the panel leaves absolute positioning and stacks under the
   document, because a 452px panel floating over a 320px window is not a
   demonstration of anything. */
@media (max-width: 860px) {
  .stage {
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.25rem;
  }
  .doc { width: 100%; }
  .pop { position: static; width: 100%; transform-origin: 50% 0; }
  .pop__rows { min-height: 66px; }
  .pop__row { flex-wrap: wrap; }
}

@media (max-width: 420px) {
  .chip { font-size: 0.6875rem; padding: 5px 8px; }
  .doc__sent, .doc__p { font-size: 0.875rem; }
  .pop__field { font-size: 0.8125rem; }
}

/* ==================================================================== feat */

.feat { padding: clamp(3rem, 8vw, 5.5rem) 0; }
.feat .h2 { margin-bottom: clamp(1.75rem, 4vw, 2.75rem); }

.bento {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}
.cell--wide { grid-column: 1 / -1; }

.cell {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: clamp(1.25rem, 3vw, 1.75rem);
}

.cell__head { display: flex; align-items: center; gap: 0.7rem; margin-bottom: 0.7rem; }

.cell__badge {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  flex: none;
  border-radius: var(--r-sm);
  background: rgba(127, 119, 221, 0.14);
  color: var(--accent);
}
.cell__badge .ico { width: 16px; height: 16px; }

.cell__body {
  color: var(--t2);
  font-size: 0.9375rem;
  line-height: 1.65;
  max-width: 54ch;
}

.tones, .langs { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 1.15rem; }

.tones li {
  padding: 0.32rem 0.7rem;
  border-radius: var(--r-sm);
  background: var(--pop-chip);
  color: var(--pop-chip-text);
  font-size: 0.8125rem;
}

.langs li {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.28rem 0.6rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-2);
  color: var(--t2);
  font-size: 0.8125rem;
}
.langs b {
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--accent);
}

.ba { margin-top: 1.15rem; display: grid; gap: 0.5rem; }
.ba__row {
  padding: 0.6rem 0.75rem;
  border-radius: 8px;
  font-size: 0.8125rem;
  line-height: 1.5;
}
.ba__row--from {
  background: var(--pop-field);
  border: 1px solid var(--line);
  color: var(--t3);
}
.ba__row--to {
  background: rgba(127, 119, 221, 0.10);
  border: 1px solid rgba(127, 119, 221, 0.26);
  color: var(--t1);
}
/* inline-block, so the placeholders can be scaled as they arrive: transform
   does nothing to a plain inline box. Neither of them is long enough to want
   to break across a line anyway. */
.ba mark {
  display: inline-block;
  background: rgba(127, 119, 221, 0.22);
  color: var(--accent-hover);
  border-radius: 4px;
  padding: 0 0.2em;
}

/* The wide cell is twice the width but carries the same amount of copy, so on
   a wide screen its tone chips move into a second column rather than leaving
   half the tile empty. */
@media (min-width: 721px) {
  .cell--wide {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
    column-gap: clamp(1.5rem, 4vw, 3rem);
    align-items: start;
  }
  .cell--wide .cell__head,
  .cell--wide .cell__body { grid-column: 1; }
  .cell--wide .tones {
    grid-column: 2;
    grid-row: 1 / span 2;
    margin-top: 0.2rem;
    align-content: flex-start;
  }
}

@media (max-width: 720px) {
  .bento { grid-template-columns: minmax(0, 1fr); }
  .cell--wide { grid-column: auto; }
}

/* =================================================================== every */

.every { padding: clamp(3rem, 8vw, 5.5rem) 0 clamp(3.5rem, 9vw, 6rem); }

.marquee {
  margin-top: clamp(2rem, 5vw, 3rem);
  overflow: hidden;
  /* Fades at both edges so the loop never hard-cuts at the viewport border. */
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

/* Two identical sets sit side by side and the track travels exactly one set's
   width, so the seam lands on itself and never shows.

   This keyframe loop is the fallback. When GSAP is running, main.js switches it
   off and drives the same travel from the ticker instead, because a keyframe
   cannot change direction with the scroll or speed up behind a flick. */
.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee 44s linear infinite;
}
@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (hover: hover) and (pointer: fine) {
  .marquee:hover .marquee__track { animation-play-state: paused; }
}

.marquee__set { display: flex; gap: 0.75rem; padding-right: 0.75rem; }
.marquee__set li {
  padding: 0.55rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--t2);
  font-size: 0.9375rem;
  white-space: nowrap;
}

/* ================================================================= pricing */

.pricing { padding: clamp(3rem, 8vw, 5.5rem) 0; }
.pricing .h2 { margin-bottom: clamp(2rem, 5vw, 2.75rem); }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: clamp(1.5rem, 4vw, 2.25rem);
}

.plans { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }

.plan {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 1.4rem;
  display: flex;
  flex-direction: column;
  transition: transform var(--d-ui) var(--ease-out),
              border-color var(--d-ui) ease,
              background-color var(--d-ui) ease;
}
.plan--lead {
  border-color: rgba(127, 119, 221, 0.45);
  background: rgba(127, 119, 221, 0.07);
}

/* A tile lifts a millimetre and warms its edge. Enough to say "this one is
   under the pointer" without competing with the button inside it, which is the
   thing that is actually meant to be clicked. */
@media (hover: hover) and (pointer: fine) {
  .plan:hover {
    transform: translateY(-3px);
    border-color: rgba(127, 119, 221, 0.4);
    background: rgba(127, 119, 221, 0.05);
  }
  .plan--lead:hover {
    border-color: rgba(127, 119, 221, 0.7);
    background: rgba(127, 119, 221, 0.11);
  }
}

.plan__tag {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--t2);
  letter-spacing: 0.01em;
}
.plan--lead .plan__tag { color: var(--accent-hover); }

.plan__price { margin-top: 0.5rem; display: flex; align-items: baseline; gap: 0.3rem; }
.plan__num {
  font-size: clamp(2rem, 5vw, 2.5rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
}
.plan__per { color: var(--t3); font-size: 0.9375rem; }

.plan__note {
  margin-top: 0.55rem;
  margin-bottom: 1.25rem;
  color: var(--t2);
  font-size: 0.875rem;
  line-height: 1.5;
}
.plan .btn { margin-top: auto; }

.incl {
  display: grid;
  gap: 0.7rem;
  margin-top: 1.75rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--line);
}
.incl li {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  color: var(--t2);
  font-size: 0.9375rem;
  line-height: 1.5;
}
.incl .ico { width: 16px; height: 16px; margin-top: 0.2em; color: var(--accent); }

.card__foot {
  margin-top: 1.6rem;
  color: var(--t3);
  font-size: 0.875rem;
  text-align: center;
}
.card__foot a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 3px; }

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

/* ===================================================================== faq */

.faq { padding: clamp(3rem, 8vw, 5.5rem) 0; }
.faq .h2 { margin-bottom: clamp(1.5rem, 4vw, 2.25rem); }

.faq__list { border-top: 1px solid var(--line); }

.q { border-bottom: 1px solid var(--line); }

.q summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  padding: 1.15rem 0;
  cursor: pointer;
  list-style: none;
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.012em;
  transition: color var(--d-fast) ease;
}
.q summary::-webkit-details-marker { display: none; }

@media (hover: hover) and (pointer: fine) {
  .q summary:hover { color: var(--accent-hover); }
}

.q__mark {
  width: 18px; height: 18px;
  flex: none;
  color: var(--t3);
  transition: transform var(--d-ui) var(--ease-out), color var(--d-fast) ease;
}
.q[open] .q__mark { transform: rotate(180deg); color: var(--accent); }

.q__a { padding-bottom: 1.35rem; display: grid; gap: 0.85rem; }
.q__a p {
  color: var(--t2);
  font-size: 0.9375rem;
  line-height: 1.7;
  max-width: 62ch;
}
.q__a a { color: var(--accent); text-underline-offset: 3px; }

/* No height animation: `details` cannot interpolate its own height in every
   browser, and a half-supported accordion jerk is worse than an honest snap.
   The content fades up instead, which is cheap and works everywhere. */
.q[open] .q__a { animation: faq-in 220ms var(--ease-out) both; }
@keyframes faq-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ===================================================================== end */

.end { padding: clamp(3rem, 8vw, 5rem) 0 clamp(4rem, 10vw, 7rem); }
.end__inner { display: grid; justify-items: center; gap: 1.5rem; text-align: center; }
.end .hero__meta { margin-top: -0.5rem; }

/* ================================================================== footer */

.foot { border-top: 1px solid var(--line); padding: 2.25rem 0 3rem; }

.foot__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 2rem;
}

.foot__links {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  margin-left: auto;
  font-size: 0.9375rem;
}
.foot__links a {
  color: var(--t2);
  text-decoration: none;
  transition: color var(--d-fast) ease;
}
@media (hover: hover) and (pointer: fine) {
  .foot__links a:hover { color: var(--t1); }
}

.foot__copy { color: var(--t3); font-size: 0.875rem; width: 100%; }

/* ================================================================== legal */

.legal { padding: clamp(2.5rem, 7vw, 4.5rem) 0 clamp(3.5rem, 9vw, 6rem); }

.legal__title {
  font-size: clamp(2rem, 5vw, 2.75rem);
  line-height: 1.1;
  letter-spacing: -0.028em;
}

.legal__date {
  margin-top: 0.9rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  color: var(--t3);
  font-size: 0.875rem;
  line-height: 1.6;
}

.legal h2 {
  margin-top: 2.5rem;
  margin-bottom: 0.7rem;
  font-size: 1.1875rem;
  letter-spacing: -0.015em;
}

.legal p, .legal li {
  color: var(--t2);
  font-size: 1rem;
  line-height: 1.75;
  max-width: 68ch;
}
.legal p + p { margin-top: 0.9rem; }

.legal__list {
  display: grid;
  gap: 0.55rem;
  margin-top: 0.9rem;
  padding-left: 1.15rem;
  list-style: disc;
}
.legal__list li { padding-left: 0.25rem; }
.legal b { color: var(--t1); font-weight: 600; }

.legal a { color: var(--accent); text-underline-offset: 3px; }

/* A button that happens to be a link is still a button. Without this, the rule
   above paints .btn--primary's label in the accent colour — on an accent
   background, i.e. invisible. It cost a rendered screenshot to notice, because
   the text was present in the DOM and occupying its full width the whole time. */
.legal a.btn { color: inherit; }
.legal a.btn--primary { color: var(--on-accent); }

/* =============================================== checkout and key delivery */

/* A quiet aside that turns into the whole message when something has gone
   wrong: the pricing card's checkout-unavailable line, and every "email us"
   footnote on the success page. Bordered rather than coloured, because it is
   read most often when nothing is wrong at all. */
/* Both selectors are needed and neither is redundant: `.legal p` above is more
   specific than a lone class, so a note inside a legal-shaped page (which is
   what success.html is) would otherwise inherit body-copy colour and size. */
.note,
.legal .note {
  margin-top: 1.5rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  color: var(--t3);
  font-size: 0.875rem;
  line-height: 1.6;
  max-width: 68ch;
}
.note a { color: var(--accent); text-underline-offset: 3px; }
.pricing .note { text-align: center; margin-inline: auto; }

.legal .ok__lead {
  margin-top: 1.1rem;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  color: var(--t2);
  font-size: 1.0625rem;
  line-height: 1.7;
  max-width: 68ch;
}

.ok__spin {
  flex: none;
  width: 15px; height: 15px;
  border-radius: 50%;
  border: 2px solid var(--t3);
  border-top-color: transparent;
  animation: spin 640ms linear infinite;
}

/* The state-specific block under the receipt line on the success page. It is a
   wrapper div, so `.legal p + p` cannot reach across it and the first sentence
   inside would otherwise sit hard against the lead above. */
.legal .ok__head { margin-top: 0.9rem; }

/* A single `.q` disclosure standing on its own, rather than as one row of the
   FAQ list. Everything else about it — the summary, the rotating chevron, the
   fade-in — is the FAQ component unchanged; only the top border it normally
   inherits from `.faq__list` and its own spacing are supplied here.

   It exists because the licence key is no longer the headline of the success
   page. A buyer who purchased from inside the app has nothing to copy, so the
   key is folded away for them and opened by default only for a web buyer, who
   is the one it is actually for. */
.q--solo {
  margin-top: 1.75rem;
  border-top: 1px solid var(--line);
}

/* `.legal p` (0,1,1) outranks `.q__a p` (0,1,1) by document order, so without
   this the disclosure's body copy silently reverts to legal-page metrics. */
.legal .q__a p {
  font-size: 0.9375rem;
  line-height: 1.7;
  max-width: 62ch;
}
.legal .q__a p + p { margin-top: 0; }   /* .q__a is a grid; its gap owns spacing */

/* The key itself. Big, monospace, selectable, and wide enough that
   TL-XXXX-XXXX-XXXX never wraps mid-group — a licence key broken across two
   lines is a licence key someone mistypes. */
.keybox {
  margin-top: 1.5rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding: 1.1rem 1.25rem;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
}

.keybox__key {
  flex: 1 1 auto;
  font: 600 clamp(1.25rem, 4.2vw, 1.75rem)/1.3 var(--mono);
  letter-spacing: 0.04em;
  color: var(--t1);
  user-select: all;
  -webkit-user-select: all;
  word-break: break-word;
}

.keybox__copy { flex: none; }

.legal .ok__said {
  margin-top: 0.65rem;
  min-height: 1.3em;              /* reserved, so a copy does not shift the page */
  color: var(--t3);
  font-size: 0.875rem;
}

.legal .ok__actions {
  margin-top: 1.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.ok__txn {
  font-family: var(--mono);
  font-size: 0.8125em;
  color: var(--t2);
  word-break: break-all;
}

@media (max-width: 420px) {
  /* Below this the key and the button stop fitting side by side; the button
     goes full width rather than becoming a sliver next to the key. */
  .keybox__copy { width: 100%; }
}

/* ================================================================= reveals */

/* The hidden state, and the fallback that undoes it.

   Everything that enters starts here: invisible, eight pixels low. GSAP takes
   over from this state and animates out of it with inline styles, which beat
   these rules without needing !important anywhere.

   The transition and the .is-in rule are what happens when GSAP is NOT there —
   a blocked request, a bad deploy — and main.js falls back to an
   IntersectionObserver that adds .is-in. That is why html.gsap switches the
   transition off: two systems writing opacity on the same element, one per
   frame and one over 260ms, produces neither.

   Gated on `.js`, which a one-line script in <head> sets before first paint.
   Written the other way round, a browser with JavaScript off would render a
   blank page and never recover, and the hidden state would flash on every load
   before the deferred script arrived. */
.js [data-enter],
.js [data-reveal] {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--d-reveal) var(--ease-out) calc(var(--i, 0) * 60ms),
              transform var(--d-reveal) var(--ease-out) calc(var(--i, 0) * 60ms);
}
.js [data-enter].is-in,
.js [data-reveal].is-in {
  opacity: 1;
  transform: translateY(0);
}
html.gsap [data-enter],
html.gsap [data-reveal] { transition: none; }

/* --- headline lines ---
   main.js rebuilds a headline as one block per line, each inside a mask, then
   slides the line up out of it.

   The padding is descender room: at line-height 1.03 the mask would otherwise
   cut the tail off a "y". The negative margin hands that room straight back, so
   a split headline is exactly as tall as the same words unsplit and putting the
   plain string back at the end of the reveal shifts nothing.

   That cancellation is why the headline becomes a flex column while it is
   split, and it is the whole reason: between two ordinary blocks, -0.08em and
   -0.2em would collapse into -0.2em rather than adding up, and every line would
   leave 0.08em of the padding behind. A flex container does not collapse its
   children's margins. Measured at 1440px: 174px tall with collapsing, 148px
   without, against 148px for the unsplit headline. */
.is-split { display: flex; flex-direction: column; }

.ln-mask {
  display: block;
  overflow: hidden;
  padding: 0.08em 0 0.2em;
  margin: -0.08em 0 -0.2em;
}
.ln-in { display: block; }

/* ======================================================== user preferences */

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

  /* Reduced motion is not no feedback. Everything that travels, scales or loops
     is removed; colour and opacity keep their timing below, because they are
     how the interface answers a hover or a press. */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
    /* Delays go too, and not for tidiness. Cutting only the durations leaves
       every stagger intact, which is the staging reduced motion asked to be
       spared: the demo's second result line carries a 560ms delay (it is line
       two of a typewriter) and the selection's second line carries 250ms, so
       the "static final frame" would spend half a second showing a truncated
       sentence — "I think we should have sent the deck", full stop. Verified:
       with only the durations cut, a full-page render of this page catches it
       mid-stagger and the second line is missing from the image entirely. */
    transition-delay: 0s !important;
    animation-delay: 0s !important;
  }

  .btn, .nav, .nav__links a, .foot__links a, .q summary, .card__foot a {
    transition-duration: var(--d-fast) !important;
  }

  [data-enter], [data-reveal] { opacity: 1 !important; transform: none !important; }
  .btn:active { transform: none; }
  .plan:hover { transform: none; }

  /* A headline that was split before the preference changed keeps its masks
     until the page is reloaded. Nothing may stay hidden inside one. */
  .ln-mask { overflow: visible; }
  .ln-in { opacity: 1 !important; transform: none !important; }
  /* The chevron keeps rotating: with the transition down to nothing it is an
     instant state change, not motion, and it is the only thing distinguishing
     an open question from a closed one. */

  /* The demo holds its finished frame: corrected sentence, panel open on the
     result. The same information, none of the movement. */
  .stage[data-state="final"] .pop { opacity: 1; transform: none; }
  .stage[data-state="final"] .ty__t { clip-path: inset(0 0 0 0); }
  .stage[data-state="final"] .ty__c { opacity: 0; }

  /* The marquee becomes what it always was underneath: a list of app names.
     The second set is only there to hide the loop's seam, so it goes. */
  .marquee { mask-image: none; -webkit-mask-image: none; overflow: visible; }
  .marquee__track {
    animation: none;
    transform: none;
    flex-wrap: wrap;
    width: auto;
    justify-content: center;
  }
  .marquee__set:last-child { display: none; }
  .marquee__set { flex-wrap: wrap; justify-content: center; gap: 0.6rem; padding-right: 0; }

  .spin { animation: none; border-top-color: var(--pop-t3); opacity: 0.5; }

  /* The success page's spinner becomes a static ring. It still has to read as
     "something is happening", because it sits next to a line of text that says
     so, and the text beside it is what actually updates. */
  .ok__spin { animation: none; border-top-color: var(--t3); opacity: 0.5; }
}

/* Frosted surfaces become solid when the user has asked for less transparency,
   rather than losing contrast to a blur that is no longer there. */
@media (prefers-reduced-transparency: reduce) {
  .nav { background: var(--bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .pop {
    background: var(--pop-panel);
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
}

@media (prefers-contrast: more) {
  :root { --t2: #C8C8CE; --t3: #B0B0B8; --line: #45454E; --line-2: #5A5A64; }
}
