/* Silky landing page — dark theme, no external fonts or CDNs. */

:root {
  --bg: #0e1116;
  --surface: #151a21;
  /* cards sit one step above the page, and catch light on their top edge */
  --surface-hi: #1b212b;
  --surface-lo: #141920;
  --text: #e6edf3;
  --muted: #9aa7b4;
  --border: #273041;
  --edge: #2a3446;     /* card hairline */
  --edge-hi: #3a475e;  /* the lit top edge */
  --accent: #7de3ff;   /* cyan */
  --accent-2: #b48cff; /* violet */
  --accent-3: #ffd166; /* amber */
  --radius: 14px;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 1.0625rem;  /* airier body, so headings can carry the contrast */
  line-height: 1.7;
}

/* Premium type is contrast, not decoration: big headings pulled tight,
   body left loose. Balance stops a wrapped headline leaving one word alone. */
h1, h2, h3 {
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

a { color: var(--accent); }

/* ---- accessibility: skip link ---- */
.skip-link {
  position: absolute;
  left: -9999px;
  padding: 0.5rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.skip-link:focus { left: 1rem; top: 1rem; z-index: 10; }

/* ---- header / nav ---- */
.site-header {
  position: sticky;
  top: 0;
  background: rgba(14, 17, 22, 0.9);
  border-bottom: 1px solid var(--border);
}

.nav {
  max-width: 960px;
  margin: 0 auto;
  padding: 0.75rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* Brand lockup: the hero's mark at header scale, beside the wordmark. Same
   geometry and gradient, so it reads as the same logo shrunk — not a second
   logo. The mark is decorative; the wordmark carries the name. */
.nav__brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 1.5rem;
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--text);
}

.nav__mark {
  width: 36px;
  height: 36px;
  flex: none;
  /* a faint violet echo of the hero's glow — static, nothing animates here */
  filter: drop-shadow(0 0 9px rgba(140, 110, 255, 0.35));
}

.nav__wordmark { line-height: 1; }

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

.nav__links a {
  color: var(--muted);
  text-decoration: none;
}
.nav__links a:hover { color: var(--text); }

/* Nav CTA — same filled-pill look as .button (shared rule below), kept
   nav-sized so header height doesn't move. Hover lift per the motion
   playbook: transform + shadow only, no pulse, no loop. */
.nav__cta {
  padding: 0.35rem 0.85rem;
  transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
}
.nav__cta:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 18px rgba(180, 140, 255, 0.4);
}
.nav__cta:active {
  transform: translateY(0) scale(0.98);
}

@media (prefers-reduced-motion: reduce) {
  .nav__cta,
  .nav__cta:hover,
  .nav__cta:active {
    transition: none;
    transform: none;
    box-shadow: none;
  }
}

.nav__toggle {
  display: none;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 0.4rem 0.8rem;
  font: inherit;
  cursor: pointer;
}

/* ---- hero ---- */
.hero {
  /* deliberate asymmetry: more air above the mark than below the button */
  padding: clamp(4.5rem, 9vw, 6.5rem) 1.25rem clamp(3.5rem, 6vw, 5rem);
  text-align: left;
  background:
    radial-gradient(600px 300px at 58% 0%, rgba(180, 140, 255, 0.12), transparent 70%),
    var(--bg);
}

.hero__inner {
  max-width: 920px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 2rem;                /* the mark and message read as one lockup */
}

.hero__visual {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

.hero__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
}

.hero__mark { width: 144px; height: 144px; }

.hero__title {
  margin: 0;
  font-size: clamp(2.6rem, 8vw, 4.25rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.035em;   /* display sizes need negative tracking */
}

.hero__promise {
  margin: 0;
  max-width: 34ch;
  color: var(--muted);
  font-size: 1.2rem;
  line-height: 1.55;
  text-wrap: pretty;
}

/* ---- buttons ---- */
/* Filled-pill treatment, one source of truth: the Download section button
   (.button) is the reference; the nav CTA borrows the same declarations so
   the two can't drift apart. .button keeps its own size + hover below. */
.button,
.nav__links a.nav__cta {
  display: inline-block;
  border-radius: 999px;
  border: none;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  background: linear-gradient(120deg, var(--accent), var(--accent-2));
  color: #0e1116;
}

.button {
  padding: 0.75rem 1.6rem;
  transition: filter 0.15s ease, transform 0.22s ease-out, box-shadow 0.22s ease-out;
}
.button:hover {
  filter: brightness(1.08);
  transform: translateY(-1px);
  box-shadow: 0 12px 26px -14px rgba(125, 227, 255, 0.55);
}

.button--ghost {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
}

/* ---- generic sections ---- */
.section {
  /* more air above a section than below its last line — even padding reads
     as a template, uneven padding reads as a decision */
  padding: clamp(3.5rem, 6vw, 5rem) 1.25rem clamp(2.75rem, 4vw, 3.5rem);
  border-top: 1px solid var(--border);
}

.section__inner {
  max-width: 960px;
  margin: 0 auto;
}

.section h2 {
  margin: 0 0 1.25rem;
  font-size: clamp(1.7rem, 3vw, 2.1rem);
  font-weight: 700;
}

.section p {
  color: var(--muted);
  max-width: 65ch;
  text-wrap: pretty;
}

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

/* ---- ambient dust (Nov 3, 2026) ----
   Two canvases drawn by one loop in script.js: a denser field inside the hero,
   a faint fixed one behind the whole page. Both are aria-hidden decoration —
   they never reach the accessibility tree and never take a click. Everything
   is layered so the dust can only ever sit behind content. */
.page-particles {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

/* the page field is z-index 0, so the page body has to stack above it —
   main first, then the header, which would otherwise be painted over on scroll */
main, .site-footer { position: relative; z-index: 1; }
.site-header { z-index: 2; }

/* overflow hides the cropped background mark (item 6 of the fix ticket) */
.hero { position: relative; overflow: hidden; }

.hero__particles {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

.hero__inner { position: relative; z-index: 1; }

/* ---- features: flip cards (Nov 3, 2026) ----
   Front = the feature, back = what's inside. The whole card is one <button>,
   so tap, Enter and Space work natively and the tap target is the card.
   Flex, not grid: a short last row has to centre itself, and grid cannot
   shift a row by half a column. Heights stay even because both faces share
   one grid cell — the shell is as tall as the taller face, never the one
   currently showing. */

.features {
  margin: 2.5rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;       /* 4 + 3: the short row centres as a group */
  gap: 1rem;
}

.feature {
  flex: 0 0 calc(25% - 0.75rem); /* four across, never grows to fill the row */
  max-width: calc(25% - 0.75rem);
  min-width: 0;
  flex-direction: column;        /* a screenshot sits under the card */
  display: flex;                 /* so the button fills the card */
  /* glass, not a box: a gradient body, a lit top edge, a shadow that only
     falls below the card */
  background: linear-gradient(180deg, var(--surface-hi), var(--surface-lo));
  border: 1px solid var(--edge);
  border-top-color: var(--edge-hi);
  border-radius: var(--radius);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.045),
    0 10px 24px -16px rgba(0, 0, 0, 0.9);
  perspective: 1200px;
}

.feature__flip {
  flex: 1;
  display: block;
  padding: 1.25rem;
  border: 0;
  border-radius: inherit;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

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

.feature__face { display: block; }

.feature__title {
  display: block;
  margin: 0 0 0.5rem;
  font-size: 1.06rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
  text-wrap: balance;   /* it's a span, so it doesn't inherit the h-rule */
  color: var(--accent);
}

.feature__copy {
  display: block;
  margin: 0;
  font-size: 0.94rem;
  line-height: 1.6;   /* denser inside a card than in the page column */
  color: var(--muted);
}

/* --- no JS: the back reads on under the front. Nothing is ever hidden. --- */
.feature__face--back {
  margin-top: 0.85rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--border);
}

/* screenshots (Part B, Nov 3, 2026): web copies live in the web root at
   ≤1200px; originals stay untouched in shots\. They sit OUTSIDE the flip
   button on purpose — reveal-on-scroll is the only motion they get, never
   the card's 3D turn. Same frame vocabulary as the cards. */
.shot {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 1rem;
  border: 1px solid var(--edge);
  border-top-color: var(--edge-hi);
  border-radius: var(--radius);
  box-shadow: 0 10px 24px -16px rgba(0, 0, 0, 0.9);
}

/* --- JS: turn the card --- */
.js .feature__flip {
  display: grid;
  transform-style: preserve-3d;
  transition: transform .6s cubic-bezier(.22, .8, .32, 1);
}

.js .feature__face {
  grid-area: 1 / 1;              /* both faces in one cell: max height wins */
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.js .feature__face--back {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  transform: rotateY(180deg);
}

.js .feature__flip[aria-expanded="true"] { transform: rotateY(180deg); }

/* the one hint that the card turns — decorative, so it stays out of the
   accessibility tree and is drawn only on the front */
.js .feature__face--front { position: relative; }
.js .feature__face--front::after {
  content: "+";
  position: absolute;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.35rem;
  height: 1.35rem;
  font-size: 1rem;
  line-height: 1;
  color: var(--accent);
  border: 1px solid var(--border);
  border-radius: 50%;
  opacity: 0.75;
}

/* reduced motion: no turn — the back replaces the front instantly, and
   visibility (not display) keeps the card the same height */
@media (prefers-reduced-motion: reduce) {
  .js .feature__flip,
  .js .feature__flip[aria-expanded="true"] {
    transform: none;
    transform-style: flat;
    transition: none;
  }
  .js .feature__face {
    transform: none;
    backface-visibility: visible;
    -webkit-backface-visibility: visible;
  }
  .js .feature__face--back { visibility: hidden; }
  .js .feature__flip[aria-expanded="true"] .feature__face--front { visibility: hidden; }
  .js .feature__flip[aria-expanded="true"] .feature__face--back { visibility: visible; }
}

/* two across on tablets, one on phones */
@media (max-width: 900px) {
  .feature {
    flex-basis: calc(50% - 0.5rem);
    max-width: calc(50% - 0.5rem);
  }
}

@media (max-width: 560px) {
  .feature {
    flex-basis: 100%;
    max-width: 100%;
  }
}

/* ---- how it works ---- */
.steps {
  margin: 2.5rem 0 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
}

.steps li {
  counter-increment: step;
  background: linear-gradient(180deg, var(--surface-hi), var(--surface-lo));
  border: 1px solid var(--edge);
  border-top-color: var(--edge-hi);
  border-radius: var(--radius);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.045),
    0 10px 24px -16px rgba(0, 0, 0, 0.9);
  padding: 1.25rem;
}

.steps li::before {
  content: counter(step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  margin-bottom: 0.5rem;
  border-radius: 50%;
  background: linear-gradient(120deg, var(--accent), var(--accent-2));
  color: #0e1116;
  font-weight: 700;
}

.steps h3 {
  margin: 0 0 0.25rem;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}
.steps p { margin: 0; font-size: 0.94rem; line-height: 1.6; }

/* ---- CLI strip (Nov 3, 2026) ----
     Two terminal shots side by side under the step cards. The images reuse
     .shot, so they carry the exact same frame, radius, border and shadow as
     every other screenshot on the page — the grid only decides how many
     sit in a row, never how they look. */
.cli {
  margin-top: 2.5rem;
}

.cli__copy {
  margin: 0 0 1rem;
  font-size: 1.1rem;
  line-height: 1.55;
  text-wrap: pretty;
}
/* .section p sets a 65ch measure; this copy wants a shorter one, and a single
   class can't outrank ".section p" — so the selector carries the section too. */
.section .cli__copy {
  max-width: 46ch;
}

.cli__shots {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  align-items: start;   /* start, not stretch: images keep their own ratio */
}

/* the grid supplies the spacing — .shot's own top margin would double it */
.cli__shots .shot { margin-top: 0; }

/* one column on phones: a terminal shot side by side is unreadable */
@media (max-width: 640px) {
  .cli__shots {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---- download ---- */
.section--download { text-align: center; }
.section--download p { margin-left: auto; margin-right: auto; }

/* ---- waitlist (Nov 3, 2026) ----
   One field, one button, sitting under the coming-soon line. The input borrows
   the pill radius and the card tokens (surface / border) so field and button
   read as one control; the submit is the page's own .button treatment, not a
   second one. Wraps to two rows below 30rem; nothing here animates. */
.waitlist {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  margin: 1.4rem auto 0;
  max-width: 32rem;
}

/* The field needs a real label; the design asks for the placeholder alone.
   Same off-screen trick as .skip-link, but this one never comes back on screen. */
.waitlist__label {
  position: absolute;
  left: -9999px;
}

.waitlist__input {
  flex: 1 1 15rem;
  min-width: 0;          /* lets the field shrink instead of overflowing */
  padding: 0.72rem 1.1rem;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.waitlist__input::placeholder { color: var(--muted); opacity: 1; }
.waitlist__input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: transparent;
}

.waitlist__status {
  margin: 1.15rem 0 0;
  font-size: 0.95rem;
  color: var(--muted);
}
/* empty until there is something to say, so it takes no space and still
   stays in the accessibility tree for the live region */
.waitlist__status:empty { margin: 0; }
.waitlist__status[data-state="error"] { color: var(--accent-3); }

@media (max-width: 30rem) {
  .waitlist__input { flex: 1 1 100%; }
  .waitlist__submit { width: 100%; }
}

/* ---- colophon (Nov 3, 2026) ----
     A signature line, not a section: same type family and weight as the
     footer line below it, just smaller and quieter. Sits above the footer's
     rule, so the credit line reads as the closing beat. */
.colophon {
  position: relative;
  z-index: 1;           /* over the fixed dust canvas, like main and the footer */
  padding: 0 1.25rem 0.5rem;
  text-align: center;
}

.colophon__line {
  margin: 0;
  max-width: 72ch;
  margin-inline: auto;
  color: var(--muted);
  opacity: 0.8;
  font-size: 0.82rem;
  line-height: 1.7;
  letter-spacing: 0.01em;
  text-wrap: pretty;
}

/* ---- footer ---- */
.site-footer {
  border-top: 1px solid var(--border);
  padding: 2.75rem 1.25rem 3rem;
  text-align: center;
  color: var(--muted);
  font-size: 0.9rem;
  letter-spacing: 0.01em;
}
.site-footer p { margin: 0; }

/* footer line: two credits, centred, with air between them.
   Below 30rem they stack and the dot drops out. */
.site-footer__line {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.25rem 0.9rem;
}

.site-footer__dot { opacity: 0.55; }

/* social line: the two accounts, a step quieter than the credits above.
   The global `a` rule paints links in the accent cyan, which is far too loud
   for a footer — these stay muted and underlined until hovered or focused. */
.site-footer .site-footer__social {
  margin-top: 0.85rem;
  font-size: 0.85rem;
}

.site-footer a {
  color: var(--muted);
  text-decoration: underline;
  text-decoration-color: rgba(154, 167, 180, 0.32);
  text-underline-offset: 0.22em;
}
.site-footer a:hover {
  color: var(--text);
  text-decoration-color: currentColor;
}
.site-footer a:focus-visible {
  color: var(--text);
  text-decoration-color: currentColor;
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}

@media (max-width: 30rem) {
  .site-footer__line {
    flex-direction: column;
    gap: 0.15rem;
  }
  .site-footer__dot { display: none; }
}

/* ---- small screens: collapse the nav ---- */
@media (max-width: 640px) {
  .nav__toggle { display: block; }

  .nav__links {
    width: 100%;
  }
  /* open/close is animated by the grid-rows rules at the bottom of this file */

  .nav { flex-wrap: wrap; }

  .hero { padding-top: 3.5rem; }
}

/* Hero mark motion — Nov 2, 2026: the true SVG, alive per part.
   Whole mark floats; strokes draw on at load; the eye blinks; the comb sways.
   Transform/opacity only; honors prefers-reduced-motion. */
.hero__mark {
  filter: drop-shadow(0 8px 26px rgba(140, 110, 255, 0.45));
  animation: mark-in 0.8s ease-out both, mark-float 5s ease-in-out 0.8s infinite;
  overflow: visible;
}
@keyframes mark-in {
  from { opacity: 0; transform: translateY(12px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes mark-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}
/* The draw-on, sway and blink belong to the hero. Scoped to .hero__mark on
   purpose: the header wears the same geometry, and without the scope it would
   inherit stroke-dashoffset: 320 and render as an invisible outline. */
.hero__mark .mark-head,
.hero__mark .mark-wattle,
.hero__mark .mark-comb .mark-stroke {
  stroke-dasharray: 320;
  stroke-dashoffset: 320;
  animation: mark-draw 1.1s ease-out forwards;
}
.hero__mark .mark-wattle { animation-delay: 0.35s; }
.hero__mark .mark-comb .mark-stroke { animation-delay: 0.15s; }
@keyframes mark-draw {
  to { stroke-dashoffset: 0; }
}
.hero__mark .mark-comb {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: comb-sway 3.6s ease-in-out 1.6s infinite alternate;
}
@keyframes comb-sway {
  from { transform: rotate(-2deg); }
  to   { transform: rotate(2.5deg); }
}
.hero__mark .mark-eye {
  transform-box: fill-box;
  transform-origin: center;
  animation: eye-blink 4.8s ease-in-out 2s infinite;
}
@keyframes eye-blink {
  0%, 92%, 100% { transform: scaleY(1); }
  95%           { transform: scaleY(0.08); }
}
@media (prefers-reduced-motion: reduce) {
  .hero__mark, .hero__mark * { animation: none !important; }
  /* These selectors MUST match the draw-on rule's specificity (0,2,0 / 0,3,0).
     A weaker reset loses the cascade: the strokes would stay at dashoffset 320
     and the mark would render as a bare eye and beak. */
  .hero__mark .mark-head,
  .hero__mark .mark-wattle,
  .hero__mark .mark-comb .mark-stroke { stroke-dashoffset: 0 !important; }
}

/* ---- motion pass — Nov 2, 2026 (web_motion playbook) ----
   Transform/opacity only (nav toggle uses the sanctioned grid-rows trick).
   Reveal styles are gated behind .js, which script.js adds on load —
   with JS off, nothing is ever hidden. Scroll reveals fire once. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    /* Delays must go too. A .45s delay is still a wait: the hero title and
       the staggered cards would sit invisible while the clock ran down
       before the (instant) fade even started. Kill the wait, keep the
       reveal — reduced motion means "no movement", not "no content". */
    animation-delay: 0s !important;
    transition-delay: 0s !important;
  }
}

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* hero text: staggered fade-up on load, landing under the mark's draw-on */
.js .hero__title, .js .hero__promise, .js .hero__inner .button {
  opacity: 0;
  animation: fadeUp .5s ease-out forwards;
}
.js .hero__title { animation-delay: .15s; }
.js .hero__promise { animation-delay: .30s; }
.js .hero__inner .button { animation-delay: .45s; }

/* scroll reveals: section content fades up at ~25% visible, once */
.js .rv {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .55s ease-out, transform .55s ease-out;
}
.js .rv.seen { opacity: 1; transform: none; }

/* cards join their section, ~90ms apart */
.js .rv .feature, .js .rv .steps li {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .45s ease-out, transform .45s ease-out, box-shadow .25s ease;
}
.js .rv.seen .feature, .js .rv.seen .steps li { opacity: 1; transform: none; }
.js .rv.seen .feature:nth-child(2), .js .rv.seen .steps li:nth-child(2) { transition-delay: .09s; }
.js .rv.seen .feature:nth-child(3), .js .rv.seen .steps li:nth-child(3) { transition-delay: .18s; }
.js .rv.seen .feature:nth-child(4) { transition-delay: .27s; }
.js .rv.seen .feature:nth-child(5) { transition-delay: .36s; }
.js .rv.seen .feature:nth-child(6) { transition-delay: .45s; }
.js .rv.seen .feature:nth-child(7) { transition-delay: .54s; }

/* hover lift: pointer devices only — phones have no hover.
   Small and short: a card that jumps 6px reads as a toy. The glow is the
   tell that it's live, not the distance.

   Specificity note (Nov 3, 2026): the scroll-reveal rule
   `.js .rv.seen .feature` is (0,4,0) and sets `transform: none`, which
   outranks a plain `.feature:hover` (0,2,0) — so the 3px lift never fired
   and only the shadow glow showed. The `.js .rv.seen …:hover` variants are
   (0,5,0) and win. The plain selectors stay for anything outside a .rv. */
@media (hover: hover) {
  .feature:hover, .steps li:hover,
  .js .rv.seen .feature:hover, .js .rv.seen .steps li:hover {
    transform: translateY(-3px);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.08),
      0 18px 36px -18px rgba(0, 0, 0, 0.95),
      0 0 0 1px rgba(125, 227, 255, 0.10);
    transition: transform 0.22s ease-out, box-shadow 0.22s ease-out;
    transition-delay: 0s;
  }
}

/* the CTA's infinite pulse was removed on Nov 3, 2026 — a looping scale reads
   as cheap on a page this quiet; the gradient and hover do the work now. */

/* reduced motion: the hover lift is motion, so it goes. Shadow stays as the
   feedback cue, because a colour change is not movement. */
@media (prefers-reduced-motion: reduce) {
  .feature:hover,
  .steps li:hover,
  .js .rv.seen .feature:hover,
  .js .rv.seen .steps li:hover,
  .button:hover { transform: none; }
}

/* smooth mobile menu: grid-rows 0fr -> 1fr, no px heights, no jump.
   Links carry the padding; lis clip, so nothing peeks when closed. */
@media (max-width: 640px) {
  /* closed state. Deliberately NOT also listed as .nav__links.is-open:
     the open rule below then wins on specificity, not on source order. */
  .nav__links {
    display: grid;
    grid-template-rows: repeat(5, 0fr);
    gap: 0;
    padding: 0;
    opacity: 0;
    transition: grid-template-rows .45s ease, opacity .35s ease;
  }
  .nav__links > li { min-height: 0; overflow: hidden; }
  .nav__links a { display: block; padding: .7rem 0; }
  .nav__links.is-open {
    grid-template-rows: repeat(5, 1fr);
    opacity: 1;
  }

  /* The open menu makes the header 353px tall, and the header's resting
     background is 90% opaque — so page content ghosted straight through the
     menu. Fully opaque only while it's open: the resting header keeps its
     translucency, the open one is a solid panel. (A2's taller tap targets
     pushed the menu far enough down to make this visible.) */
  .site-header:has(.nav__links.is-open) {
    background: #0e1116;
  }

  /* the CTA is a pill — keep it hugging its label instead of stretching
     to the full row width with no side padding */
  .nav__links .nav__cta {
    display: inline-block;
    margin: .3rem 0;
    padding: .35rem .95rem;
  }
}

/* Touch targets — Nov 3, 2026.
   Measured in headless Edge at 375px and 640px: the brand lockup was 36px
   tall, the Menu button 43.7px, the nav CTA 40.1px — all just under the
   44px a thumb wants. min-height grows the hit area without moving the
   layout, so nothing else on the page shifts. */
@media (max-width: 640px) {
  .nav__brand { min-height: 44px; }

  .nav__toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0.4rem 0.9rem;
  }

  .nav__links .nav__cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
  }
}

/* ==== About page — Nov 2, 2026 ==== */

.about-hero {
  padding: 4.5rem 1.25rem 3.5rem;
  background:
    radial-gradient(680px 380px at 32% 30%, rgba(125, 227, 255, 0.10), transparent 70%),
    radial-gradient(620px 360px at 78% 65%, rgba(180, 140, 255, 0.12), transparent 70%),
    var(--bg);
}

.about-hero__inner {
  max-width: 960px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: 3rem;
  align-items: center;
}

.about-hero__copy h1 {
  margin: 0 0 0.75rem;
  font-size: clamp(2rem, 5vw, 2.9rem);
}

/* One eyebrow rule: the numerals are decorative structure (aria-hidden in the
   markup so a screen reader hears the heading first) and never take the 65ch
   text measure. */
.eyebrow {
  margin: 0 0 0.35rem;
  max-width: none;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-2);
}

.about-hero__lede {
  margin: 0;
  max-width: 46ch;
  font-size: 1.1rem;
  color: var(--muted);
}

/* body copy stays on the same 960px grid as the hero; the existing
   .section p { max-width: 65ch } keeps the line length readable */
.about-body h2 {
  margin: 2.75rem 0 0.75rem;
  font-size: 1.5rem;
}
.about-body h2:first-child { margin-top: 0; }

/* content placeholder: obviously a slot, so no invented copy slips through */
.slot {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  background: rgba(255, 255, 255, 0.015);
  color: var(--muted);
  font-style: italic;
}

@media (max-width: 760px) {
  .about-hero { padding-top: 3rem; }
  .about-hero__inner {
    grid-template-columns: 1fr;
    gap: 2rem;
    text-align: center;
  }
  .about-hero__lede { margin-inline: auto; }
}

/* ==== Silky in 3D — Nov 2, 2026 ====
   The flat SVG faked into a turning object: each part (rim, comb, wattle,
   head, beak, eye) is its own layer on its own Z plane inside a
   preserve-3d stage, so a slow rotateY reads as volume instead of a card
   flip. Transform only — no images, no libraries, no per-frame JS.
   Depth knobs: the translateZ values on the --rim/--comb/... rules. */

.silky3d {
  position: relative;
  width: clamp(190px, 26vw, 280px);
  aspect-ratio: 1 / 1;
  margin-inline: auto;
  perspective: 1000px;         /* long lens — catalog-honest, no wide-angle stretch */
  perspective-origin: 50% 45%;
}

/* the fixed 3/4 tilt: without it a spin looks like a flat mirror flip */
.silky3d__tilt {
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transform: rotateX(14deg) rotateZ(-6deg);
}

.silky3d__stage {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  animation: silky-spin var(--silky-turn, 16s) linear infinite;
  will-change: transform;
}

@keyframes silky-spin {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}

/* Alternative motion, if the full turn ever feels too much: add
   class="silky3d silky3d--pivot" in about.html. Same mark, but it pivots
   ±70° and never turns edge-on. */
.silky3d--pivot .silky3d__stage {
  animation: silky-pivot 9s ease-in-out infinite alternate;
}
@keyframes silky-pivot {
  from { transform: rotateY(-70deg); }
  to   { transform: rotateY(70deg); }
}

.silky3d__layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* depth, back to front — deliberately small: this is line art, so a big
   Z spread makes the beak and eye slide off the face instead of turning */
.silky3d__layer--rim       { transform: translateZ(-7px); }
.silky3d__layer--fill-c    { transform: translateZ(-6px); }
.silky3d__layer--fill-b    { transform: translateZ(-4px); }
.silky3d__layer--fill-a    { transform: translateZ(-2px); }
.silky3d__layer--comb      { transform: translateZ(-4px); }
.silky3d__layer--wattle    { transform: translateZ(-2px); }
.silky3d__layer--head      { transform: translateZ(0); }
.silky3d__layer--beak      { transform: translateZ(4px); }
.silky3d__layer--beak-fill { transform: translateZ(2px); }
.silky3d__layer--eye       { transform: translateZ(6px); }

/* The studio — key-light bloom plus a contact shadow, sitting behind the
   object and outside the turning stage: light does not spin with the thing
   it lights. (Playbook: no contact shadow = the #1 "that looks CG" tell.) */
.silky3d__backdrop {
  position: absolute;
  inset: -14%;
  border-radius: 50%;
  background:
    radial-gradient(58% 26% at 50% 84%, rgba(3, 5, 10, 0.8), transparent 72%),
    radial-gradient(circle at 50% 44%,
      rgba(180, 140, 255, 0.30),
      rgba(125, 227, 255, 0.10) 45%,
      transparent 70%);
  filter: blur(14px);
}

/* let a pointer user stop the turn and look at it */
@media (hover: hover) {
  .silky3d:hover .silky3d__stage { animation-play-state: paused; }
}

@media (prefers-reduced-motion: reduce) {
  .silky3d__stage {
    animation: none;
    transform: rotateY(-28deg);
  }
}

/* ==== Hero: the mark as a big background presence — Nov 3, 2026 ====
   The About page's 3D stack, blown up past the hero on both axes and dropped
   behind the copy. Same markup, same Z-spread, same studio backdrop: nothing
   here flattens, outlines or recolours it. The differences are scale, a
   face-on resting pose, and the pointer response.

   It reads at rest: the object faces the viewer so comb, ring and beak are
   recognisable at a glance — the turn is only a few degrees on top of that.
   Transform and opacity only — no layout properties, no filter animation.

   Shifted to the right by measurement: the giant ring sits behind the copy
   side of the hero, but far enough right that its band never crosses the
   headline or promise at rest. The foreground mark owns the left edge; the
   sculpture supplies depth behind the message rather than competing with it. */

.hero__mark-bg {
  position: absolute;
  top: 50%;
  left: 78%;                 /* behind the copy, clear of the message */
  width: auto;
  /* Large overlay: the ring's band passes outside the lockup on both sides,
     and the aperture is the space the copy sits in. 300% rather than 320%:
     the lockup is a fixed 920px block, so just above the 1400px tier the
     bigger mark left only 5px of air — at 300% the same width clears by 25px
     and the difference at 1920 is invisible. */
  height: 300%;
  aspect-ratio: 1 / 1;
  margin: 0;
  transform: translate(-50%, -50%);
  pointer-events: none;      /* the layer never takes a click */
  z-index: 0;
  opacity: 0.45;             /* atmosphere — the foreground mark stays the logo */
}

/* face the viewer: no resting yaw, no resting pitch. The bulge comes from the
   layer stack's own Z spread, which reads at 0deg */
.hero__mark-bg .silky3d__stage {
  animation: none;
  transform: none;
}

/* the pointer turn rides on top of that face-on pose. Vars are written by
   script.js; with no script, or under reduced motion, they are unset and the
   fallback here is the resting pose. */
.hero__mark-bg .silky3d__tilt {
  transform:
    translate3d(var(--hero-px, 0px), var(--hero-py, 0px), 0)
    rotateX(var(--hero-rx, 0deg))
    rotateY(var(--hero-ry, 0deg))
    rotateZ(-4deg);          /* a hint of life, nothing more */
}

/* the halo comes up as the pointer works the hero */
.hero__mark-bg .silky3d__backdrop {
  opacity: var(--hero-glow, 0.72);
}

/* the mark is cropped by the hero on every edge; the bottom edge is the one
   that reads as a mistake, because it lands on a section boundary. So the
   bottom of the object dissolves into the hero's own background instead of
   being sliced. Painted above the mark (z 0) and below the copy (z 1): the
   lockup never dims, only the background object does. */
.hero::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 30%;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent 0%, var(--bg) 85%);
}

/* desktop up to 1400: the lockup is a fixed 920px block, so between roughly
   1101 and 1400 the viewport is wide enough for the two-column layout but not
   yet wide enough for the 320% mark to clear it — at 78% the band's inner edge
   lands on the tail of the promise line (measured: 2-6 glyph samples, 0px of
   air). Pull it in and push it right across that whole range; above 1400 the
   base rule clears on its own and grows past 1400 as the viewport widens. */
@media (min-width: 901px) and (max-width: 1400px) {
  .hero__mark-bg {
    left: 84%;
    height: 260%;
    opacity: 0.38;
  }
}

/* small screen / tablet: the foreground mark stacks above centred copy. The
   giant background mark stays centred and very faint — the message has to
   win, and the smaller viewport crops more of the object, so it gives way
   harder. */
@media (max-width: 900px) {
  .hero {
    text-align: center;
    background:
      radial-gradient(600px 300px at 50% 0%, rgba(180, 140, 255, 0.12), transparent 70%),
      var(--bg);
  }

  .hero__inner {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 1.25rem;
  }

  .hero__copy {
    align-items: center;
  }

  .hero__mark {
    width: 128px;
    height: 128px;
  }

  .hero__mark-bg {
    left: 50%;
    height: 260%;
    opacity: 0.25;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero__mark-bg .silky3d__tilt {
    transform: rotateZ(-4deg);
  }
  .hero__mark-bg .silky3d__backdrop { opacity: 0.72; }
}

/* Fix 3 (QA, Nov 2): anchor jumps must land below the sticky header.
   --header-h is measured from the live header by script.js; 60px is the
   static fallback for no-JS loads (nav padding + tallest child + border). */
#about, #features, #how, #download {
  scroll-margin-top: calc(var(--header-h, 60px) + 16px);
}

