/* Inter, self-hosted.

   Chosen over the system font for character, and served from our own origin
   rather than from Google's CDN. Analytics already reaches a third party, but
   that is one the visitor can decline; a webfont is not declinable, so putting
   it on someone else's server would mean every visitor's IP reaching Google
   whether they consented or not, on a site whose whole argument is about not
   handing your data around.

   Latin only, three weights, 142KB total. The latin-ext faces were dropped:
   they cover accented characters this site never renders and were the larger
   half of the payload. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================
   MacGaze · "Instrument"

   The product turns a face into a measurement, so the site is built like a
   measuring device. That is not decoration: this page has to say "not as
   secure as Face ID, it stores your password, a video might fool it", and in
   a glossy aesthetic those read as apologies. Stated as instrument
   specifications, candour becomes the brand instead of the liability.

   Two grounds alternate as CHAPTERS, not as a theme: pale panel to explain,
   deep night for the moment of recognition. Neutrals carry a slight green
   bias throughout so they read as chosen rather than defaulted.

   All type is system type. The only external measurement request is the
   consent-controlled Google tag described in the Privacy Policy.
   ============================================================ */

:root {
  /* Panel: the instrument face */
  --paper: #F0F2EC;
  --paper-2: #E6E9E1;
  --card: #FBFCF9;
  --ink: #12150F;
  --ink-2: #383E37;
  --ink-3: #5A6158;
  --rule: #D3D7CC;
  --rule-2: #C0C5B8;
  --signal: #1B7F38;
  --signal-quiet: rgba(27, 127, 56, 0.10);
  --amber: #8A5A08;
  --amber-quiet: rgba(138, 90, 8, 0.10);

  /* Night: the moment */
  --night: #0A0C0B;
  --night-2: #141816;
  --night-3: #1D2220;
  --night-ink: #E6EAE5;
  --night-ink-2: #ADB5AC;
  --night-rule: #262B27;
  --glow: #30D158;

  --sans: "Inter", -apple-system, BlinkMacSystemFont, system-ui,
          "Segoe UI", sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  --serif: ui-serif, "New York", "Iowan Old Style", Georgia, serif;

  /* Built-in easings are too weak to read as deliberate. These are the
     three the whole page uses, and nothing rolls its own. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-move: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1);

  --measure: 33rem;
  --gutter: 1.75rem;
  --page: 74rem;
}

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

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 18px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ---------- structure ---------- */

/* Tightened from 9.5rem. The page was running to seven thousand pixels, and
   generous is not the same as empty: past a point the gaps stop reading as
   composure and start reading as unfinished. */
.chapter { padding: clamp(3.75rem, 8vw, 7rem) 0; position: relative; }
.chapter--night { background: var(--night); color: var(--night-ink); }
.chapter--tight { padding-top: clamp(3rem, 6vw, 5rem); }

/* The opening screen has to be the only thing on it. Letting the next
   chapter's pale ground peek in at the bottom tells the visitor the page is
   a list of sections before they have read a word of the first one. svh
   rather than vh so mobile browser chrome cannot push it into an overflow. */
.chapter--hero {
  /* The sticky nav sits in the flow above this, so a flat 100svh overshoots by
     exactly the nav's height and pushes the scroll cue off the bottom. --nav-h
     is measured in JS, and the fallback keeps this sane before it lands. */
  min-height: calc(100vh - var(--nav-h, 3.6rem));
  min-height: calc(100svh - var(--nav-h, 3.6rem));
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: clamp(4.5rem, 9vw, 7rem);
  padding-bottom: clamp(3.5rem, 7vw, 6rem);
}

/* A quiet mark that there is more below, since a full-height first screen
   removes the usual cue of a section edge poking up. */

.wrap {
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.stack { display: flex; flex-direction: column; }
.stack > * { margin: 0; }

.gap-xs { gap: 0.5rem; }
.gap-s  { gap: 0.9rem; }
.gap-m  { gap: 1.6rem; }
.gap-l  { gap: 2.6rem; }
.gap-xl { gap: 4rem; }

.row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.85rem; }

/* ---------- analytics consent ---------- */

.consent {
  position: fixed;
  z-index: 20;
  left: 1rem;
  right: 1rem;
  bottom: 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.2rem;
  max-width: 56rem;
  margin: 0 auto;
  padding: 1rem 1.1rem;
  border: 1px solid rgba(48, 209, 88, 0.35);
  border-radius: 1rem;
  background: rgba(20, 24, 22, 0.96);
  color: var(--night-ink);
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.24);
  font-size: 0.82rem;
  line-height: 1.35;
}

/* The author-level flex display must not override the hidden attribute. */
.consent[hidden] { display: none; }

.consent-copy {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.7rem;
}

.consent-copy span { color: var(--night-ink-2); }
.consent-copy a { color: var(--glow); }
.consent-actions { display: flex; flex-shrink: 0; gap: 0.5rem; }
.consent-actions .btn { padding: 0.55rem 0.8rem; font-size: 0.75rem; }
.consent-actions .btn--ghost {
  color: var(--night-ink);
  border-color: rgba(230, 234, 229, 0.55);
}
.consent-actions .btn--ghost:hover {
  background: rgba(230, 234, 229, 0.12);
  border-color: var(--night-ink);
}

@media (max-width: 42rem) {
  .consent { align-items: stretch; flex-direction: column; }
  .consent-actions { display: grid; grid-template-columns: 1fr 1fr; }
}

/* ---------- type ---------- */

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

/* Wispr-scale confidence, delivered by a grotesque rather than a serif. */
.display {
  /* 5.5rem, not 6. Inter sets wider than SF Pro at the same size, and at 6rem
     "Open the lid." needed 538px in a 537px column: one pixel over, which is
     the difference between two lines and three. Left some room this time. */
  font-size: clamp(2.7rem, 7vw, 5.5rem);
  line-height: 0.94;
  letter-spacing: -0.04em;
}

.display--sm {
  font-size: clamp(2.1rem, 5vw, 3.9rem);
  line-height: 1.0;
  letter-spacing: -0.032em;
}

h3 { font-size: 1.09rem; line-height: 1.35; letter-spacing: -0.012em; }

p { margin: 0; max-width: var(--measure); }

/* The instrument's voice. Promoted well beyond caption duty. This is where
   most of the page's personality lives. */
.label {
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.chapter--night .label { color: var(--night-ink-2); }

.lede {
  font-size: clamp(1.1rem, 1.9vw, 1.32rem);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 30rem;
}
.chapter--night .lede { color: var(--night-ink-2); }

.small { font-size: 1.02rem; line-height: 1.58; color: var(--ink-2); }
.chapter--night .small { color: var(--night-ink-2); }
.tiny { font-size: 0.92rem; color: var(--ink-2); line-height: 1.55; }
.chapter--night .tiny { color: var(--night-ink-2); }

.aside {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1.3rem, 2.6vw, 1.85rem);
  line-height: 1.35;
  letter-spacing: -0.01em;
  max-width: 26rem;
}

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.sig { color: var(--signal); }
.chapter--night .sig { color: var(--glow); }

/* ---------- the notch ---------- */

/* Pure black against a lighter ground: a notch only reads as a notch when the
   screen around it is lighter than it is, exactly as on real hardware. */
/* The lid is the only piece of hardware on the page, so it has to read as
   hardware: a screen with something on it, a bezel catching light, and a
   shadow underneath. A flat grey rectangle reads as a placeholder. */
.lid-stage {
  /* Larger than before because the corrected notch ratio is small by nature:
     a real Mac's island is a modest thing against a whole screen. Scaling the
     lid up keeps it legible without lying about the proportion. */
  width: min(100%, 34rem);
  /* Closer perspective makes the same tilt angle read as real depth rather
     than a flat swivel. Paired with the stronger tilt angle in app.js. */
  perspective: 800px;
  perspective-origin: 50% 40%;
}

.lid {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10.5;
  background:
    radial-gradient(120% 90% at 50% -10%, rgba(48, 209, 88, 0.10), transparent 58%),
    radial-gradient(90% 70% at 22% 108%, rgba(80, 120, 100, 0.13), transparent 62%),
    linear-gradient(168deg, #1C2220 0%, #101413 52%, #0B0E0D 100%);
  border: 1px solid #2E3531;
  border-radius: 16px 16px 5px 5px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    inset 0 0 40px rgba(0, 0, 0, 0.5),
    0 30px 60px -30px rgba(0, 0, 0, 0.9);
  /* Three rotations compose here: the cursor tilt, and the scroll-driven turn.
     Kept as separate custom properties so the pointer handler and the scroll
     handler never overwrite each other's work. */
  transform: rotateX(calc(var(--tilt-x, 0deg) + var(--spin-x, 0deg)))
             rotateY(calc(var(--tilt-y, 0deg) + var(--spin-y, 0deg)));
  transform-style: preserve-3d;
  transition: transform 0.5s var(--ease-out), box-shadow 0.6s var(--ease-out);
  will-change: transform;
}

/* Following the cursor exactly feels mechanical, so the tilt is smoothed in
   JS and the transition here only covers the return to rest. */
.lid[data-tracking="true"] { transition: box-shadow 0.6s var(--ease-out); }

/* Recognition spills light onto the screen below the notch, the way a real
   display does. This is what sells the moment more than the tick alone. */
.lid::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(70% 46% at 50% 4%,
              rgba(48, 209, 88, 0.30), rgba(48, 209, 88, 0.07) 46%, transparent 72%);
  opacity: 0;
  transition: opacity 0.7s var(--ease-out);
}
.lid[data-glow="true"]::after { opacity: 1; }

.notch {
  /* Straight off the hardware. A 14 inch MacBook Pro is 1512x982pt with a
     180x37pt notch, so 11.9% by 3.77%. Every previous value here was a guess
     that came out too tall. */
  width: 11.9%;
  min-width: 44px;
  height: 3.77%;
  min-height: 11px;
  background: #000;
  position: relative;
  border-radius: 0 0 8px 8px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  padding-bottom: 0;
  /* The Dynamic Island curve: it overshoots very slightly on the way open,
     which is what makes a piece of hardware feel like it stretched rather
     than resized. */
  transition: width 0.58s var(--ease-spring),
              height 0.52s var(--ease-spring),
              border-radius 0.5s var(--ease-out),
              box-shadow 0.5s var(--ease-out);
  box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.7);
  z-index: 2;
}

.notch[data-open="true"] {
  /* Opening stretches it in both directions, which is the whole appeal of the
     Dynamic Island: the hardware appears to give way rather than a panel
     appearing over it.

     Sized from what the real thing actually does. On a 14 inch MacBook the
     notch is about 180x37pt on a 1512x982pt screen, and the app expands it to
     250x124, so roughly 17% of the width and 13% of the height. The previous
     47% by 62% was a curtain coming down over half the display, which is why
     it read as the app taking over the screen rather than the hardware making
     a little room. The status text moved out onto the screen below for the
     same reason: the real island holds the glyph and nothing else. */
  /* The app expands to 250x124pt on that same 1512x982 screen, so 16.5% by
     12.63%. Matching the app exactly rather than matching an iPhone: this is a
     Mac, and the panel here should be the one the Mac actually draws. */
  width: 16.5%;
  min-width: 62px;
  height: 12.63%;
  /* Low enough that the percentage governs at desktop sizes and only takes
     over on genuinely small screens, where 12.63% would be unreadable. */
  min-height: 38px;
  border-radius: 0 0 16px 16px;
  box-shadow: 0 14px 34px -14px rgba(0, 0, 0, 0.85);
}

/* Bottom aligned, which is what the app does, and it is not a style choice.
   The top of this panel is where the physical notch actually is: a real Mac
   has camera housing there, not screen. Anything drawn in that band would be
   hidden behind the hardware.

   Positioned rather than padded, because a percentage padding resolves against
   the containing block's WIDTH even when it is being used to place something
   vertically. This panel is wider than it is tall, so padding-bottom: 12.9%
   silently came out a third too large. `bottom` and `height` percentages both
   resolve against height, which is what was wanted. */
.notch-body {
  position: absolute;
  inset: 0;
  gap: 0;
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 0.3s ease 0.12s, transform 0.4s cubic-bezier(0.34,1.4,0.5,1) 0.12s;
}
.notch[data-open="true"] .notch-body { opacity: 1; transform: scale(1); }

/* ---------- the glyph, and the sequence it plays ----------

   One element, five phases, and the same choreography the app runs. See
   docs/design/animation-spec.md.

     idle      nothing
     searching the Finder face, eyes glancing, light sweeping down it
     orb       the face becomes a ring, and the ring flips through a sphere
     collapse  the sphere folds back into a single circle
     tick      the tick draws inside it

   The unlock is deliberately not "the face fades and a tick appears". That was
   the first attempt and it threw away the most recognisable half of what an
   iPhone actually does, which is the flip. */

.face {
  /* The app draws a 62pt glyph in a 124pt panel, so half the panel's height,
     sitting 16pt off the bottom, which is 12.9% of that height. */
  position: absolute;
  left: 50%;
  bottom: 12.9%;
  transform: translateX(-50%);
  height: 50%;
  aspect-ratio: 1;
}
.glyph { width: 100%; height: 100%; overflow: visible; display: block; }
.glyph path, .glyph circle, .glyph ellipse {
  fill: none;
  stroke: var(--glow);
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---- the face ---- */

.g-dim { opacity: 0.26; }
.g-dim path, .g-lit path { stroke-width: 5.2; }

/* Only light moves during the search. Geometry that rotates demands the eye
   follow it; light travelling over a still object can be ignored, which is
   what makes it liveable on the twentieth viewing. */
.g-lit {
  -webkit-mask-image: linear-gradient(transparent, #000 40%, #000 60%, transparent);
  mask-image: linear-gradient(transparent, #000 40%, #000 60%, transparent);
  -webkit-mask-size: 100% 200%;
  mask-size: 100% 200%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  opacity: 0;
}
[data-phase="searching"] .g-lit { opacity: 1; animation: sweep 1.5s ease-in-out infinite; }
@keyframes sweep {
  0%   { -webkit-mask-position: 0 -100%; mask-position: 0 -100%; }
  100% { -webkit-mask-position: 0 100%;  mask-position: 0 100%; }
}

/* The eyes glance left, right, then settle. Nothing else on the face moves. */
[data-phase="searching"] .g-eyes {
  animation: glance 1.5s var(--ease-move) infinite;
  transform-origin: 50px 50px;
}
@keyframes glance {
  0%   { transform: translateX(0); }
  22%  { transform: translateX(-3.2px); }
  46%  { transform: translateX(3.2px); }
  72%  { transform: translateX(-1.4px); }
  100% { transform: translateX(0); }
}

/* Recognition: the features collapse inward and the brackets rotate away as
   the ring arrives, so the frame reads as BECOMING the ring rather than being
   swapped for it. */
.g-dim, .g-lit { transition: opacity 0.2s ease, transform 0.3s var(--ease-move); transform-origin: 50px 50px; }
[data-phase="orb"] .g-dim, [data-phase="collapse"] .g-dim, [data-phase="tick"] .g-dim,
[data-phase="orb"] .g-lit, [data-phase="collapse"] .g-lit, [data-phase="tick"] .g-lit {
  opacity: 0;
  transform: rotate(45deg) scale(0.72);
}

/* ---- the unlock ----

   This is no longer a recreation. Four attempts at rebuilding the reference by
   measurement got closer each time and never got there, which was the signal
   that measuring was the wrong approach: the exact file was already to hand.
   `unlock.svg` IS Macario's original, with two edits and nothing else. The
   Dynamic Island pill was removed, because our notch already is that pill and
   two of them nest into a black slab inside a black slab. And the viewBox was
   cropped to the orb so it fills the panel.

   Loaded as an <object> rather than inlined because the file is 73KB and there
   are two of them on the page, and rather than an <img> because an object can
   be driven: seeking it, starting it and freezing it on its final frame are all
   possible through its contentDocument, and none of them are through an img. */

.unlock-anim {
  position: absolute;
  left: 50%;
  top: 50%;
  /* The reference draws its circle at 82% of its cropped box; ours sat at 68%
     of the glyph box. Scaling by that ratio lands the ring in exactly the place
     the Finder face's brackets just left, so the handover does not jump. */
  width: 83%;
  height: 83%;
  transform: translate(-50%, -50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}
[data-phase="orb"] .unlock-anim,
[data-phase="collapse"] .unlock-anim,
[data-phase="tick"] .unlock-anim { opacity: 1; }

/* ---- refusal ----
   No ring, no flip, no tick. The tick means yes, so it never appears here. */
[data-phase="refused"] .g-dim,
[data-phase="refused"] .g-lit { opacity: 1; transform: none; }
[data-phase="refused"] .g-dim path,
[data-phase="refused"] .g-lit path { stroke: #8C948C; }
[data-phase="refused"] .g-lit { animation: none; }

/* The status word sits on the screen below the island, which is where the
   device puts it. Keeping it inside meant the notch had to be big enough to
   contain a sentence, which is what forced the old oversized geometry. */
.lid-word {
  margin-top: 0.7rem;
  max-width: 100%;
  text-align: center;
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgba(230, 234, 229, 0.72);
  white-space: nowrap;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.3s ease, transform 0.4s var(--ease-out);
  pointer-events: none;
}
.lid[data-open="true"] .lid-word {
  opacity: 1;
  transform: none;
  transition-delay: 0.16s;
}

.shake { animation: shake 0.42s ease; }
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  22% { transform: translateX(-6px); }
  46% { transform: translateX(6px); }
  70% { transform: translateX(-4px); }
  88% { transform: translateX(3px); }
}

@media (prefers-reduced-motion: reduce) {
  [data-phase="searching"] .g-lit,
  [data-phase="searching"] .g-eyes,
  /* The embedded animation is SMIL, which prefers-reduced-motion cannot
     touch from here, so it is simply not shown: the tick state is reached
     without it. */
  .unlock-anim { display: none; }
}

/* ---------- controls ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font: inherit;
  font-size: 0.97rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  padding: 0.82rem 1.45rem;
  border-radius: 11px;
  border: 1.5px solid transparent;
  cursor: pointer;
  text-decoration: none;
  transition: transform 0.16s var(--ease-out), background 0.18s ease,
              border-color 0.18s ease, box-shadow 0.22s var(--ease-out);
}
/* A button that does not move when pressed does not feel like it heard you. */
.btn:active { transform: scale(0.972); }

@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover { box-shadow: 0 6px 20px -8px rgba(27, 127, 56, 0.55); }
  .btn--ghost:hover { border-color: var(--ink-3); }
}

.btn--primary { background: var(--signal); color: #F4FBF5; }
.btn--primary:hover { background: #17692F; }
.chapter--night .btn--primary { background: var(--glow); color: #04150A; }
.chapter--night .btn--primary:hover { background: #4ADC6C; }

.btn--ghost { background: transparent; color: var(--ink); border-color: var(--rule-2); }
.btn--ghost:hover { background: var(--paper-2); }
.chapter--night .btn--ghost { color: var(--night-ink); border-color: var(--night-rule); }
.chapter--night .btn--ghost:hover { background: var(--night-2); }

/* Segmented control, styled like a physical instrument switch.

   The selected state is a single thumb that TRAVELS between the two options
   rather than one background switching off while another switches on. A
   blinking segmented control tells you the state changed; a sliding one shows
   you which way it went, and the eye follows it without being asked. Its
   geometry is measured in JS and handed back as --thumb-x / --thumb-w, so the
   two labels can be different lengths and the thumb still fits exactly. */
.switch {
  position: relative;
  display: inline-flex;
  /* Inside a column flex parent, inline-flex still gets stretched. The control
     has to hug its own labels or the thumb reads as a bar with text in it. */
  align-self: flex-start;
  max-width: 100%;
  padding: 4px;
  gap: 4px;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 13px;
  isolation: isolate;
}

.switch-thumb {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 0;
  width: var(--thumb-w, 0px);
  border-radius: 9px;
  background: var(--card);
  box-shadow: 0 1px 2px rgba(18, 21, 15, 0.10),
              0 4px 12px -4px rgba(18, 21, 15, 0.16);
  transform: translateX(var(--thumb-x, 4px));
  transition: transform 0.42s var(--ease-spring), width 0.42s var(--ease-spring);
  z-index: -1;
  opacity: 0;
}
.switch[data-ready="true"] .switch-thumb { opacity: 1; }

.switch button {
  position: relative;
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.62rem 1.05rem;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
  transition: color 0.26s ease, transform 0.14s var(--ease-out);
}
.switch button:active { transform: scale(0.965); }
.switch button[aria-pressed="true"] { color: var(--ink); }

/* No JS: fall back to the flat two-state control rather than no state at all. */
.no-js .switch button[aria-pressed="true"],
.switch:not([data-ready="true"]) button[aria-pressed="true"] {
  background: var(--card);
  box-shadow: 0 1px 2px rgba(18, 21, 15, 0.09);
}

/* A hard text swap under the demo reads as a bug. Blur bridges the gap so the
   eye sees one sentence becoming another rather than two sentences trading
   places. */
.morph { transition: opacity 0.2s ease, filter 0.2s ease; }
.morph[data-swapping="true"] { opacity: 0; filter: blur(3px); }

/* ---------- panels + cards ---------- */

.card {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 1.6rem 1.7rem;
  transition: transform 0.34s var(--ease-out), box-shadow 0.34s var(--ease-out),
              border-color 0.34s ease;
}
@media (hover: hover) and (pointer: fine) {
  .card:hover {
    transform: translateY(-3px);
    border-color: var(--rule-2);
    box-shadow: 0 18px 40px -28px rgba(18, 21, 15, 0.4);
  }
}
.chapter--night .card {
  background: var(--night-2);
  border-color: var(--night-rule);
}

.grid { display: grid; gap: 1rem; }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); }

/* Section intros. A label, a heading and a lede stacked full width filled 41%
   of the measure and left a void down the right of every single section. That
   is the specific thing that reads as unbalanced rather than as minimal: not
   the amount of space, but all of it being on one side.

   Two columns, baseline aligned, so the heading and its lede sit as one line of
   thought across the width instead of a column with nothing beside it. */
.intro {
  display: grid;
  gap: clamp(1.2rem, 3vw, 3rem);
  grid-template-columns: 1fr;
}
@media (min-width: 56rem) {
  .intro {
    grid-template-columns: 1.3fr 1fr;
    align-items: end;
    gap: clamp(2rem, 5vw, 4.5rem);
  }
  /* Sits on the heading's baseline rather than floating above it. */
  .intro .lede { max-width: 25rem; padding-bottom: 0.3rem; }
}

.split {
  display: grid;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 62rem) {
  .split { grid-template-columns: 1.05fr 1fr; }
  .split--reverse > *:first-child { order: 2; }
}

/* ---------- spec sheet ---------- */

.spec { border-top: 1px solid var(--rule); }
.spec-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.35rem;
  padding: 1.15rem 0;
  border-bottom: 1px solid var(--rule);
}
@media (min-width: 44rem) {
  .spec-row { grid-template-columns: 13rem 1fr; gap: 2rem; align-items: baseline; }
}
.spec-row dt {
  font-family: var(--mono);
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.spec-row dd { margin: 0; max-width: var(--measure); }
/* Zoom on hover, done on the compositor.
   The old version animated padding-left, which is a layout property: every
   frame re-ran layout for the row and everything after it, which is exactly
   what made it judder. transform and opacity are the only two properties that
   skip layout and paint entirely, so the same movement becomes smooth. */
.spec-row {
  transition: transform 0.32s var(--ease-out),
              background-color 0.32s ease,
              box-shadow 0.32s var(--ease-out);
  transform-origin: left center;
  will-change: transform;
  border-radius: 10px;
}
@media (hover: hover) and (pointer: fine) {
  .spec-row:hover {
    background-color: rgba(27, 127, 56, 0.05);
    transform: scale(1.012);
    box-shadow: 0 10px 30px -22px rgba(18, 21, 15, 0.5);
  }
}
@media (prefers-reduced-motion: reduce) {
  .spec-row:hover { transform: none; }
}

/* ---------- readouts ---------- */

.readout {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.readout b { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.02em; }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 0.74rem;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  padding: 0.34rem 0.66rem;
  border-radius: 6px;
  background: var(--signal-quiet);
  color: var(--signal);
}
.pill--amber { background: var(--amber-quiet); color: var(--amber); }
.chapter--night .pill { background: rgba(48,209,88,0.14); color: var(--glow); }

/* ---------- the ring ---------- */

.ring-stage {
  position: relative;
  width: min(100%, 21rem);
  aspect-ratio: 1;
  margin-inline: auto;
  display: grid;
  place-items: center;
  touch-action: none;
}

.ring-eye {
  width: 56%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  display: grid;
  place-items: center;
}

.ring-tick {
  position: absolute;
  width: 3px;
  border-radius: 2px;
  background: var(--rule-2);
  transform-origin: 50% 50%;
  transition: background 0.18s ease;
}
.ring-tick[data-lit="true"] { background: var(--signal); }
/* Pointer tracking sets height every move, so it must not be animated there.
   Only the self-filling pass (touch, and the nudge) gets easing. */
.ring-tick[data-eased="true"] {
  transition: background 0.18s ease, height 0.34s var(--ease-spring),
              transform 0.34s var(--ease-spring), margin-top 0.34s var(--ease-spring);
}
.ring-stage { cursor: crosshair; }
.ring-eye {
  transition: transform 0.5s var(--ease-out), box-shadow 0.5s var(--ease-out);
}
.ring-stage[data-full="true"] .ring-eye {
  transform: scale(1.045);
  box-shadow: 0 0 0 1px var(--signal), 0 10px 30px -18px rgba(27, 127, 56, 0.6);
}

/* ---------- privacy diagram ---------- */

.diagram {
  position: relative;
  border: 1.5px solid var(--ink);
  border-radius: 18px;
  padding: 1.5rem;
  background: var(--card);
}
.diagram-label {
  position: absolute;
  top: -0.7rem;
  left: 1.2rem;
  padding: 0 0.5rem;
  background: var(--paper);
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.chip-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.diagram { transition: border-color 0.4s ease, box-shadow 0.4s var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .diagram:hover { box-shadow: 0 20px 46px -34px rgba(18, 21, 15, 0.45); }
  .chip { transition: transform 0.24s var(--ease-out), border-color 0.24s ease; }
  .chip:hover { transform: translateY(-2px); border-color: var(--rule-2); }
}
.chip {
  font-family: var(--mono);
  font-size: 0.79rem;
  padding: 0.44rem 0.7rem;
  border-radius: 8px;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  color: var(--ink-2);
}

/* ---------- timeline ---------- */

.tl { display: flex; flex-direction: column; }
.tl-item {
  display: grid;
  grid-template-columns: 4.6rem 1fr;
  gap: 1.1rem;
  padding: 0.85rem 0;
  border-top: 1px solid var(--night-rule);
  align-items: baseline;
}
.tl-item:first-child { border-top: 0; }

/* The section is about a sequence, so it arrives as a sequence. A green mark
   travels down the left edge as each row lands. */
.tl-item {
  opacity: 0;
  transform: translateY(10px);
}
.tl[data-in="true"] .tl-item {
  opacity: 1;
  transform: none;
  transition: opacity 0.42s ease, transform 0.52s var(--ease-out);
}
.tl[data-in="true"] .tl-item:nth-child(1) { transition-delay: 0.00s; }
.tl[data-in="true"] .tl-item:nth-child(2) { transition-delay: 0.11s; }
.tl[data-in="true"] .tl-item:nth-child(3) { transition-delay: 0.22s; }
.tl[data-in="true"] .tl-item:nth-child(4) { transition-delay: 0.33s; }
.tl[data-in="true"] .tl-item:nth-child(5) { transition-delay: 0.44s; }
.tl[data-in="true"] .tl-item:nth-child(6) { transition-delay: 0.55s; }
.tl[data-in="true"] .tl-item:nth-child(7) { transition-delay: 0.66s; }
@media (prefers-reduced-motion: reduce) {
  .tl-item { opacity: 1; transform: none; }
}
.no-js .tl-item { opacity: 1; transform: none; }

/* Same asymmetry as the reveals: cascade in, leave together. */
.tl-item { transition: opacity 0.22s ease, transform 0.26s var(--ease-out); }

/* The row you are reading brightens. Cheap, and it makes a static list feel
   like something you are moving through. */
@media (hover: hover) and (pointer: fine) {
  .tl-item { transition: opacity 0.42s ease, transform 0.52s var(--ease-out), background 0.2s ease; }
  .tl[data-in="true"] .tl-item:hover { background: rgba(48, 209, 88, 0.045); }
}
.tl-t {
  font-family: var(--mono);
  font-size: 0.84rem;
  font-variant-numeric: tabular-nums;
  color: var(--glow);
}

/* ---------- nav + footer ---------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 40;
  background: rgba(10, 12, 11, 0.82);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s ease, background 0.3s ease;
}
.nav[data-scrolled="true"] {
  border-bottom-color: var(--night-rule);
  background: rgba(10, 12, 11, 0.92);
}
.nav-in {
  transition: padding 0.3s var(--ease-out);
}
.nav[data-scrolled="true"] .nav-in { padding-block: 0.62rem; }

/* A single hairline showing how far down the page you are. The product is a
   measuring instrument, so the page gets a gauge. */
.gauge {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 4px;
  width: 100%;
  transform: scaleX(var(--progress, 0));
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--signal), var(--glow));
  opacity: 0;
  transition: opacity 0.3s ease;
}
.nav[data-scrolled="true"] .gauge { opacity: 1; }

.nav-in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem var(--gutter);
  max-width: var(--page);
  margin: 0 auto;
  flex-wrap: wrap;
}
.brand svg { transition: transform 0.5s var(--ease-spring); }
@media (hover: hover) and (pointer: fine) {
  .brand:hover svg { transform: scale(1.14); }
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--night-ink);
  text-decoration: none;
}
.nav-links { display: flex; gap: 1.15rem; flex-wrap: wrap; align-items: center; }
.nav-links a {
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--night-ink-2);
  text-decoration: none;
  transition: color 0.16s ease;
}
.nav-links a:hover { color: var(--night-ink); }

a { color: var(--signal); }
.chapter--night a { color: var(--glow); }
a:hover { text-decoration: underline; }

:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 3px;
  border-radius: 4px;
}
.chapter--night :focus-visible, .nav :focus-visible { outline-color: var(--glow); }

.foot {
  background: var(--night);
  color: var(--night-ink-2);
  padding: 3.5rem 0 4rem;
  font-size: 0.88rem;
}
.foot a { color: var(--night-ink); text-decoration: none; }
.foot a:hover { text-decoration: underline; }

/* ---------- reveal ---------- */

/* Reveals run in both directions. Scrolling back up puts things away again,
   which is what makes the page feel like one continuous thing rather than a
   set of tricks that each fire once. The exit is deliberately about a third
   of the entrance: arriving is the moment worth drawing out, leaving is not. */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.26s ease, transform 0.3s var(--ease-out);
}
.reveal[data-in="true"] {
  opacity: 1;
  transform: none;
  transition: opacity 0.72s ease var(--delay, 0s),
              transform 0.82s var(--ease-out) var(--delay, 0s);
}

/* Rows inside a revealed block arrive one after another. Everything landing
   at once reads as a page load; a short cascade reads as a page arriving. */
.stagger > * {
  opacity: 0;
  transform: translateY(12px);
  /* No per-child delay on the way out, or putting a six-row block away takes
     longer than reading it. */
  transition: opacity 0.22s ease, transform 0.26s var(--ease-out);
}
.stagger[data-in="true"] > * {
  opacity: 1;
  transform: none;
  transition: opacity 0.5s ease, transform 0.6s var(--ease-out);
}
.stagger[data-in="true"] > *:nth-child(1)  { transition-delay: 0.02s; }
.stagger[data-in="true"] > *:nth-child(2)  { transition-delay: 0.07s; }
.stagger[data-in="true"] > *:nth-child(3)  { transition-delay: 0.12s; }
.stagger[data-in="true"] > *:nth-child(4)  { transition-delay: 0.17s; }
.stagger[data-in="true"] > *:nth-child(5)  { transition-delay: 0.22s; }
.stagger[data-in="true"] > *:nth-child(6)  { transition-delay: 0.27s; }
.stagger[data-in="true"] > *:nth-child(7)  { transition-delay: 0.32s; }
.stagger[data-in="true"] > *:nth-child(8)  { transition-delay: 0.37s; }
@media (prefers-reduced-motion: reduce) {
  .stagger > * { opacity: 1; transform: none; }
}
.no-js .stagger > * { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; }
}
.no-js .reveal { opacity: 1; transform: none; }

/* ---------- tables + long-form legal ---------- */

.tablewrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.93rem; }
th, td { text-align: left; padding: 0.62rem 0.8rem; border-bottom: 1px solid var(--rule); }
th {
  font-family: var(--mono); font-weight: 500; font-size: 0.66rem;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);
}
tr:last-child td { border-bottom: none; }

/* Legal pages are a reading column, and it should sit in the middle of the
   page rather than being pinned to the left of a container three times its
   width. It was filling 45% of the measure with 628px of nothing beside it,
   which is why the line breaks looked arbitrary: the text was wrapping at 33rem
   for legibility while the eye could see there was clearly room for more.

   The column is centred and widened to 42rem. Still a comfortable line length,
   about 85 characters, but it no longer looks like a mistake. */
.doc {
  padding: 3.5rem 0 2rem;
  max-width: 42rem;
  margin-inline: auto;
}
.doc h1 {
  font-size: clamp(2.2rem, 5vw, 3.2rem);
  letter-spacing: -0.035em;
  line-height: 1.04;
  margin-bottom: 0.2rem;
}
.doc h2 {
  font-size: 1.45rem;
  letter-spacing: -0.022em;
  margin-top: 3rem;
  line-height: 1.25;
}
.doc h3 { margin-top: 1.9rem; font-size: 1.12rem; }
.doc p, .doc li { max-width: none; }
.doc { line-height: 1.68; }

/* Tables get the full width back, since a table is not a reading column and
   squeezing three columns into a paragraph measure is what made them cramped. */
.doc .tablewrap {
  margin-inline: calc(50% - 50vw);
  padding-inline: max(var(--gutter), calc(50vw - var(--page) / 2));
  max-width: 100vw;
}
@media (max-width: 46rem) {
  .doc .tablewrap { margin-inline: 0; padding-inline: 0; }
}
.doc .tiny, .doc td, .doc th { color: var(--ink-2); }
.doc > p, .doc > ul, .doc > div { margin-top: 0.9rem; }
.doc ul { padding-left: 1.1rem; display: flex; flex-direction: column; gap: 0.45rem; }
.doc li::marker { color: var(--ink-3); }
.doc code {
  font-family: var(--mono); font-size: 0.88em;
  background: var(--paper-2); padding: 0.1em 0.35em; border-radius: 4px;
}
.doc blockquote {
  margin: 1.5rem 0;
  padding: 1.1rem 1.3rem;
  border-left: 2px solid var(--amber);
  background: var(--card);
  border-radius: 0 10px 10px 0;
  color: var(--ink-2);
  font-size: 0.94rem;
}
.doc blockquote p { max-width: none; }
.doc strong { color: var(--ink); }


/* ---------- the sign-off lockup ---------- */

/* Wispr Flow ends on an enormous wordmark whose W animates as sound waves,
   which works because the mark IS the product. Ours does the same trick with
   the thing MacGaze actually does: the glyph searches, then recognises. It
   plays once when it scrolls into view, which on this page means at the very
   end, as a full stop rather than another thing competing for attention.

   Sized in vw so it spans the measure at any width. That is deliberate: a
   fixed-size mark in a wide container is what leaves the big empty side
   panels this page is meant to avoid. */
.lockup {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.6rem, 1.6vw, 1.4rem);
  padding: clamp(1.5rem, 4vw, 3.5rem) 0 clamp(2rem, 5vw, 4rem);
}

.lockup-word {
  font-size: clamp(3rem, 15.5vw, 13rem);
  font-weight: 600;
  letter-spacing: -0.055em;
  line-height: 0.8;
  color: var(--night-ink);
  /* Optical alignment: the cap height of the word sits slightly above the
     glyph's box, so nudge the word rather than the glyph, which has to stay
     square for the animation to read as a face. */
  transform: translateY(-0.03em);
}

.lockup-glyph {
  width: clamp(3rem, 15vw, 12.5rem);
  height: auto;
  flex: none;
  overflow: visible;
  fill: none;
  stroke: var(--glow);
  stroke-width: 5.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Brackets draw themselves on, corner by corner. 150 comfortably exceeds the
   real path length, so the dash fully covers it without measuring each one. */
.lg-brackets path {
  stroke-dasharray: 150;
  stroke-dashoffset: 150;
}
.lockup[data-phase="searching"] .lg-brackets path,
.lockup[data-phase="found"] .lg-brackets path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.55s var(--ease-out);
}
.lockup[data-phase="searching"] .lg-brackets path:nth-child(2) { transition-delay: 0.07s; }
.lockup[data-phase="searching"] .lg-brackets path:nth-child(3) { transition-delay: 0.14s; }
.lockup[data-phase="searching"] .lg-brackets path:nth-child(4) { transition-delay: 0.21s; }

/* The face itself fades up once the frame exists to hold it. */
.lg-face { opacity: 0; transition: opacity 0.3s ease; }
.lockup[data-phase="searching"] .lg-face { opacity: 1; transition-delay: 0.42s; }
.lockup[data-phase="found"] .lg-face { opacity: 0; transition-duration: 0.22s; }

/* The searching beat: eyes glance left, then right, then settle. This is the
   "looking around" from the second reference, reduced to the one movement
   that reads at a glance. Only the eyes move; a whole face sliding about
   looks like a sticker being dragged rather than something looking. */
.lockup[data-phase="searching"] .lg-eyes {
  animation: lg-glance 1.5s var(--ease-move) 0.5s both;
}
@keyframes lg-glance {
  0%   { transform: translateX(0); }
  22%  { transform: translateX(-3.2px); }
  46%  { transform: translateX(3.2px); }
  72%  { transform: translateX(-1.4px); }
  100% { transform: translateX(0); }
}

/* Recognition: the face steps aside and the tick takes its place, one glyph
   at a time, exactly as the app behaves. */
.lg-tick {
  stroke-dasharray: 80;
  stroke-dashoffset: 80;
}
.lockup[data-phase="found"] .lg-tick {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.42s var(--ease-out) 0.18s;
}

/* The whole mark gives one small nod of acknowledgement on success. */
.lockup[data-phase="found"] .lockup-glyph {
  animation: lg-settle 0.5s var(--ease-spring) 0.18s;
}
@keyframes lg-settle {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.045); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .lockup[data-phase="searching"] .lg-eyes,
  .lockup[data-phase="found"] .lockup-glyph { animation: none; }
}

/* Resting state, reached through black rather than by crossfading.

   Macario was specific and he is right: a direct crossfade has both marks
   faintly visible at once, which looks like a printing error. Out to nothing,
   a beat of nothing, then in. The pause is what makes it read as two separate
   statements rather than one smearing into the other. */
.lockup[data-phase="fading"] .lg-tick { opacity: 0; transition: opacity 0.3s ease; }
.lockup[data-phase="fading"] .lg-face { opacity: 0; transition: opacity 0.3s ease; }
.lockup[data-phase="fading"] .lg-brackets path { stroke-dashoffset: 0; }

.lockup[data-phase="settled"] .lg-brackets path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.4s var(--ease-out);
}
.lockup[data-phase="settled"] .lg-face {
  opacity: 1;
  transition: opacity 0.42s ease;
}
.lockup[data-phase="settled"] .lg-tick { opacity: 0; }

/* The tick fades rather than un-draws: un-drawing reads as it being rubbed
   out, and nothing here is being undone. */
.lg-tick { opacity: 1; }

/* No JS: show the resolved mark rather than an invisible one. */
.no-js .lg-brackets path { stroke-dashoffset: 0; }
.no-js .lg-face { opacity: 1; }


/* ============================================================
   Phone and desktop are two different products

   Not two stylesheets, but a real boundary, drawn once and honoured:

     Phone      No cursor exists, so nothing may depend on one. Nothing
                hover-only, nothing that has to be pointed at to be
                discovered. Motion is limited to what arrives on scroll,
                because a thumb is already busy scrolling.
     Desktop    There is a cursor and a mouse wheel, so the page can afford
                to answer both. The lid turns as you scroll and leans toward
                the pointer.

   The switch is `hover: hover` and `pointer: fine` rather than a width, since
   width does not tell you whether there is a cursor: a small window on a Mac
   still has one, and a large tablet does not.
   ============================================================ */

/* ---- phone ---- */

@media (hover: none), (pointer: coarse) {
  /* A tap target that only reveals itself on hover is invisible here, so the
     lid says plainly that it can be tapped. */
  .lid { cursor: default; }

  /* Slightly larger, since it is being read at arm's length on a small screen
     rather than at desk distance. */
  .lid-word { font-size: 0.68rem; }

  /* Nothing on a phone should wait to be pointed at. */
  .spec-row:hover, .card:hover, .chip:hover { transform: none; box-shadow: none; }
}

/* ---- desktop ---- */

@media (hover: hover) and (pointer: fine) {
  /* The hero MacBook turns as the page scrolls, which is the one piece of
     scroll-driven motion on the page. It is here rather than everywhere
     because the lid is the product: turning it shows the hardware the app
     runs on, and turning anything else would just be movement.

     --spin-x and --spin-y are written by the scroll handler in app.js. */
  .lid-stage { perspective: 1400px; }
}

@media (prefers-reduced-motion: reduce) {
  .lid { --spin-x: 0deg !important; --spin-y: 0deg !important; }
}


/* ---------- the demo is a close-up ----------

   The hero lid is drawn at true scale, which means its notch is small: a real
   island is a modest thing against a whole screen, and showing that honestly
   was the point of correcting the ratio. But at that scale the glyph lands at
   about 21px, and nobody can see an animation at 21px.

   So the two surfaces answer two different questions. The hero says "this is
   how much of your screen it uses", and has to stay in proportion to be worth
   anything. The demo says "this is what it does", and is a close-up, cropped to
   the top of a screen the way a product shot would be. The panel keeps the real
   250:124 and 180:37 aspect ratios; only the magnification changes. */

.lid--closeup {
  aspect-ratio: 16 / 6;
  border-radius: 16px 16px 4px 4px;
}
.lid--closeup .notch {
  width: 152px;
  height: 31px;
  min-width: 0;
  min-height: 0;
}
.lid--closeup .notch[data-open="true"] {
  width: 210px;
  height: 104px;
  min-width: 0;
  min-height: 0;
  border-radius: 0 0 26px 26px;
}
.lid--closeup .lid-word { font-size: 0.66rem; margin-top: 0.9rem; }

@media (max-width: 34rem) {
  .lid--closeup .notch { width: 122px; height: 25px; }
  .lid--closeup .notch[data-open="true"] { width: 172px; height: 85px; }
}


/* ---------- click the face, it spins ----------

   Every Finder face on the page is clickable and gives a small spin back. Not
   a feature, just the kind of thing that rewards someone poking at it. The
   whole mark turns once, quickly, and lands where it started.

   On the mark itself rather than on a wrapper so it works wherever the glyph
   appears: the nav, the footer lockup, the ring's centre, the notch. */
.spinnable { cursor: pointer; }
.spinnable > svg, svg.spinnable {
  transform-origin: 50% 50%;
  will-change: transform;
}
.spinnable[data-spin="true"] > svg,
svg.spinnable[data-spin="true"] {
  animation: face-spin 0.72s cubic-bezier(0.34, 1.28, 0.42, 1);
}
@keyframes face-spin {
  0%   { transform: rotate(0deg) scale(1); }
  35%  { transform: rotate(150deg) scale(0.9); }
  100% { transform: rotate(360deg) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .spinnable[data-spin="true"] > svg,
  svg.spinnable[data-spin="true"] { animation: none; }
}


/* ---------- click the face, it flips ----------

   A coin flip about the vertical axis: the face turns edge-on and comes back,
   rather than spinning round in the plane of the page. rotateY, not rotate.

   Smoothness comes from three things, and the first attempt had none of them:
   the glyph is promoted to its own compositor layer with translateZ, nothing
   but the rotation animates, and there is no scale to change the size the
   layer has to be rasterised at mid-turn. An SVG this size re-rasterised from
   its paths every frame is exactly the stutter that showed up half way
   through the turn. */
.spinnable { cursor: pointer; }

.spinnable > svg,
svg.spinnable {
  transform-origin: 50% 50%;
  transform: translateZ(0);
  backface-visibility: visible;
}

.spinnable[data-spin="true"] > svg,
svg.spinnable[data-spin="true"] {
  animation: face-flip 0.7s cubic-bezier(0.45, 0, 0.25, 1);
  will-change: transform;
}

@keyframes face-flip {
  from { transform: perspective(600px) translateZ(0) rotateY(0deg); }
  to   { transform: perspective(600px) translateZ(0) rotateY(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .spinnable[data-spin="true"] > svg,
  svg.spinnable[data-spin="true"] { animation: none; }
}

/* The trial badge in the hero. Bigger and brighter than a normal pill because
   it is the offer, not a footnote: it was previously stated in 13px grey under
   the buttons, where small print goes, so the page read as wanting money up
   front. */
.pill--lead {
  align-self: flex-start;
  font-size: 0.82rem;
  padding: 0.42rem 0.85rem;
  background: rgba(48, 209, 88, 0.16);
  color: var(--glow);
  border: 1px solid rgba(48, 209, 88, 0.3);
  letter-spacing: 0.06em;
}


/* The thank-you page's opening. One column, centred: this page has a single
   thing to say, and a product shot beside it would invite comparison instead
   of reading. */
.thanks-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 1rem;
}
.thanks-hero .lockup { padding: 0 0 1.5rem; }
.thanks-hero .lockup-word { font-size: clamp(2.4rem, 9vw, 6rem); }
.thanks-hero .lockup-glyph { width: clamp(2.4rem, 8.6vw, 5.6rem); }
