/* ZYNLO — marketing site
 *
 * ── WHAT THIS IS, AND WHAT IT IS NOT ────────────────────────────────────────
 * A static marketing site for zynlo.app. It deliberately shares NOTHING with
 * the React application at app.zynlo.app except the brand: no bundle, no
 * components, no design-token import. The two surfaces deploy independently,
 * so a copy change here must never require shipping a new application build.
 *
 * The palette is sampled from the actual logo master rather than invented:
 * the mark's dominant colours are #100020 through #402060, with the lit fold
 * around #6B3FA0. Everything below is derived from those.
 *
 * ⚠️ THE PRODUCT IS THE IMAGERY. This stylesheet is deliberately quiet. If a
 * section needs decoration to be interesting, the section is wrong.
 */

:root {
  /* ── ground ────────────────────────────────────────────────────────────
     A warm STONE white, not cream. design.js in the app makes the same
     move and flags the same risk: Chartilo's theme forbids warm grounds
     because they read brown, so the warmth here is a few degrees of hue at
     very low saturation — three points of red over blue on the canvas, six
     on the alternate. It reads as paper under a lamp rather than as white
     under a fluorescent, and it lands nowhere near beige.
     Pure white is kept as a SURFACE token: product surfaces sit on the
     ground rather than being it, which is what makes them read as objects. */
  --paper:      #FBFAF8;
  --paper-2:    #F5F3EF;
  --paper-3:    #ECE8E2;
  --surface:    #FFFFFF;

  /* ── ink ───────────────────────────────────────────────────────────────
     Warm near-black to match the ground. A blue-black on a warm paper is
     the specific mismatch that makes a page look assembled from two kits. */
  --ink:        #1A1518;
  --ink-2:      #56505A;
  /* ⚠️ Darkest value clearing 4.5:1 on BOTH grounds — the same token is used
     on --paper and --paper-2, so it has to pass on the darker of the two. */
  --ink-3:      #6A636F;

  /* ── THE MATERIAL ──────────────────────────────────────────────────────
     One deep matte aubergine, treated as a physical material rather than as
     an accent colour. Depth comes from overlap, a tiny edge highlight and a
     small tonal darkening at folds — NOT from gradients.
     Sampled from the logo master, whose folded faces run #160823 through
     #341751 with the lit fold near #5E3391.
     Budget: this should cover well under 10% of the page. It is important
     because it is rare. */
  --zp-deep:    #160823;
  --zp:         #25103A;
  --zp-lit:     #341751;
  --zp-edge:    rgba(255,255,255,.10);
  --zp-ink:     #F4F0F7;
  --zp-ink-2:   #BCAACE;

  /* Kept as aliases so nothing downstream had to be renamed. */
  --aubergine:  var(--zp);
  --aubergine-2:var(--zp-lit);

  /* The interactive accent: links, the lit state of things. Not a surface. */
  --purple:     #5E3391;
  --purple-soft:#F4F0F9;
  --purple-line:#E3D9EF;

  /* Hairlines, warmed to sit on the stone ground. */
  --rule:       #D8D3CC;
  --rule-2:     #EAE6E0;

  --measure:    62ch;
  --col:        1080px;
  --gutter:     clamp(22px, 5vw, 40px);

  /* ── type ──────────────────────────────────────────────────────────────
     TWO families, deliberately.

     The sans is system-resolved — SF Pro, Segoe, Roboto — and carries body,
     controls, product UI, navigation and metadata. No webfont, no FOUT, and
     it is already the typeface this audience reads everything else in.

     The display face is Source Serif 4, self-hosted from the repo's existing
     @fontsource dependency (SIL OFL, redistributable). It is NOT decoration:
     it is the face the app already sets lesson reading in, so a heading here
     and a passage in the product are the same voice. It is a contemporary
     screen serif — intelligent and editorial without being a fashion
     magazine or a university press.
     It is used ONLY for the hero and major section headings. Everything else
     stays sans, which is what keeps it from becoming a serif website. */
  --sans:    -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --display: "Source Serif 4", Georgia, "Times New Roman", serif;
  --serif:   var(--display);
  --mono:    ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --radius:    12px;
  --radius-lg: 18px;
  /* The app's study card is R.xl = 22. A flashcard anywhere on this site
     uses the same number, because it is meant to be the same object. */
  --radius-card: 22px;

  /* ── shadow ────────────────────────────────────────────────────────────
     Paper sitting a few millimetres above paper: soft, wide, and tinted with
     the material rather than neutral grey, so a shadow on the stone ground
     does not go blue. Never a floating-SaaS drop shadow. */
  --shadow:      0 1px 2px rgba(32,18,48,.04), 0 8px 24px -12px rgba(32,18,48,.12);
  --shadow-lg:   0 2px 4px rgba(32,18,48,.05), 0 24px 60px -24px rgba(32,18,48,.20);
  --shadow-card: 0 1px 1px rgba(32,18,48,.05), 0 14px 34px -18px rgba(32,18,48,.24);
}

/* Source Serif 4 — SIL Open Font License 1.1. Self-hosted: no font service,
   no third-party request, no layout shift beyond the swap. ONE weight: nothing on the
   page sets the display face at anything but 600, and a face that needs six
   weights is doing too many jobs.
   See assets/fonts/source-serif-4-LICENSE.txt. */
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/source-serif-4-latin-600-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* clip, not hidden: hidden makes the other axis a scroll container and
     breaks position:sticky further down the page. */
  overflow-x: hidden;
  overflow-x: clip;
}

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

/* ── skip link ───────────────────────────────────────────────────────────── */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--aubergine); color: #fff;
  padding: 12px 18px; border-radius: 0 0 var(--radius) 0;
  font-weight: 600; text-decoration: none;
}
.skip:focus { left: 0; }

/* ── focus, everywhere, visible ──────────────────────────────────────────── */
:focus-visible {
  outline: 2px solid var(--purple);
  outline-offset: 3px;
  border-radius: 4px;
}
/* ⚠️ THE RING HAS TO INVERT ON THE MATERIAL. --purple against the closing
   surface measures 1.94:1, under SC 1.4.11's 3:1 floor for a focus
   indicator, and there are two real CTAs inside that section — a keyboard
   user would have been tabbing into something they could not see. */
.sky :focus-visible, .close :focus-visible { outline-color: var(--paper); }

/* ── layout ──────────────────────────────────────────────────────────────── */
.wrap { width: 100%; max-width: var(--col); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: clamp(72px, 10vw, 140px); }
.section--tight { padding-block: clamp(40px, 6vw, 72px); }
.section--paper2 { background: var(--paper-2); }
.section--dark { background: #000; color: #fff; }

/* ── type ────────────────────────────────────────────────────────────────── */
/* Apple's move, and the reason its pages read as expensive: headings are
   LARGE and tracked TIGHT, with weight doing the work a second family used
   to do. -0.03em at 64px is a different object from -0.01em. */
h1, h2, h3, h4 { margin: 0; color: var(--ink); font-weight: 600; }

/* The display face, on the two levels that carry the page's voice.
   ⚠️ TRACKING IS NOT TRANSFERABLE BETWEEN FAMILIES. The sans headings above
   ran at -0.032em, which a grotesque at 72px needs and a serif at 72px does
   not: Source Serif already sets tight, and that value closed the counters
   and made the serifs collide. -0.016em is the equivalent optical setting. */
h1, h2 { font-family: var(--display); font-weight: 600; font-variant-ligatures: common-ligatures; }
h1 { font-size: clamp(36px, 6.4vw, 76px); line-height: 1.03; letter-spacing: -0.016em; }
h2 { font-size: clamp(27px, 5vw, 49px); line-height: 1.09; letter-spacing: -0.014em; }
/* h3 stays sans: it labels product UI and sub-parts, which is the sans's job.
   A serif every three lines is how a site becomes a serif site. */
h3 { font-family: var(--sans); font-size: clamp(19px, 2vw, 22px); line-height: 1.22; letter-spacing: -0.017em; font-weight: 600; }
p  { margin: 0; }

.lede      { font-size: clamp(18px, 2.2vw, 22px); line-height: 1.45; color: var(--ink-2); max-width: 52ch; letter-spacing: -0.012em; }
.body      { color: var(--ink-2); max-width: var(--measure); }
.small     { font-size: 15px; color: var(--ink-2); }
/* The app's one uppercase device (design.js `type.label`: mono, 11px, 0.1em,
   uppercase, "used sparingly") borrowed for section eyebrows. It reads as
   part of the product rather than as a marketing kicker — and it takes ten
   instances of purple off the page, which is most of why the purple now
   feels like it means something. */
.eyebrow   {
  font-family: var(--mono);
  font-size: 11.5px; font-weight: 500; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3);
  margin: 0 0 14px;
}
.eyebrow--quiet { color: var(--ink-3); }

/* ── buttons ─────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  min-height: 48px; padding: 0 24px;
  border-radius: 999px; border: 1px solid transparent;
  font: inherit; font-size: 16px; font-weight: 600; letter-spacing: -0.008em;
  text-decoration: none; cursor: pointer;
  transition: transform .18s ease, background-color .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.btn--primary { background: var(--aubergine); color: #fff; box-shadow: none; }
.btn--primary:hover { background: var(--aubergine-2); }
.btn--ghost { background: transparent; color: var(--aubergine); border-color: var(--rule); }
.btn--ghost:hover { border-color: var(--aubergine); background: var(--purple-soft); }
.btn--sm { min-height: 40px; padding: 0 17px; font-size: 15px; }

/* A CTA whose destination does not exist yet says so, and is not clickable.
   ⚠️ This is the honest state, not a styling variant: shipping a dead link to
   a store listing or an undeployed subdomain is worse than shipping nothing. */
.btn[aria-disabled="true"] {
  pointer-events: none; cursor: default;
  background: transparent; color: var(--ink-3);
  border-color: var(--rule); box-shadow: none;
}
.btn__note { font-size: 12px; font-weight: 500; color: var(--ink-3); letter-spacing: 0; }

@media (prefers-reduced-motion: reduce) {
  .btn, .btn:hover { transform: none; transition: none; }
}

/* ── header ──────────────────────────────────────────────────────────────── */
.site-header {
  position: sticky; top: 0; z-index: 40;
  /* Tinted to the ground, not to white: a white bar over a stone page reads
     as a panel sitting on top rather than as the page's own edge. */
  background: rgba(251,250,248,.82);
  backdrop-filter: saturate(160%) blur(12px);
  -webkit-backdrop-filter: saturate(160%) blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color .2s ease;
}
.site-header[data-scrolled="true"] { border-bottom-color: var(--rule-2); }
.site-header__in { display: flex; align-items: center; gap: 26px; min-height: 70px; }
.brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; color: var(--ink); flex-shrink: 0; }
/* The mark is swapped by replacing this one file. Nothing else references it. */
.brand img { width: 29px; height: 29px; border-radius: 7px; }
/* The wordmark in the display face — the one brand-adjacent use §6 allows.
   Set optically: the serif needs none of the tracking the grotesque wanted. */
.brand span { font-family: var(--display); font-weight: 600; font-size: 20px; letter-spacing: -0.012em; }

.site-nav { display: flex; gap: 26px; margin-inline: auto; }
.site-nav a { color: var(--ink-2); text-decoration: none; font-size: 15px; font-weight: 500; }
.site-nav a:hover { color: var(--ink); }
.header-cta { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

@media (max-width: 860px) {
  .site-nav { display: none; }
  .header-cta .btn--ghost { display: none; }
}

/* ── hero ────────────────────────────────────────────────────────────────── */
.hero { padding-block: clamp(56px, 8vw, 118px) clamp(64px, 9vw, 132px); }
.hero__grid { display: grid; gap: clamp(46px, 6vw, 76px); align-items: center; }
@media (min-width: 940px) { .hero__grid { grid-template-columns: 1.04fr .96fr; } }
.hero h1 { max-width: 13ch; }
.hero .lede { margin-top: 22px; max-width: 44ch; }
.hero__cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; align-items: center; }
.hero__meta {
  margin-top: 30px; font-size: 15px; font-weight: 400; letter-spacing: -0.008em;
  color: var(--ink-3);
}

/* ── THE DECK ────────────────────────────────────────────────────────────
 * The hero's signature object: three deep-purple card BACKS fanned behind one
 * live white FACE. It is the mark's own idea — folded, overlapping cards — in
 * the product's own material, and it says what the product is before a word of
 * copy is read: a deck, with one card in front of you.
 *
 * ⚠️ THE FAN LIVES IN PADDING, NOT IN OVERFLOW. The backs are absolutely
 * positioned to the deck's PADDING BOX and then translated into the padding,
 * so the whole fan is inside the element's own border box. Translating them
 * out of an unpadded box instead would push the widest card past the viewport
 * edge on a phone, and `overflow-x: clip` on the body would silently amputate
 * it rather than report a problem.
 */
.hero__stack { position: relative; }
.deck {
  position: relative;
  --fan: clamp(6px, 1.4vw, 10px);
  /* Room for the fan is TOP and RIGHT, because that is where it opens. */
  padding: calc(var(--fan) * 3.4) calc(var(--fan) * 3.4) 0 0;
}
.deck__back {
  position: absolute;
  inset: calc(var(--fan) * 3.4) calc(var(--fan) * 3.4) 0 0;
  /* Rotate about the bottom-left corner, the way a held deck opens. Rotating
     about the centre instead pushed the backs DOWN-right, where they read as
     a coloured drop shadow rather than as cards behind a card. */
  transform-origin: 0% 100%;
  border-radius: var(--radius-card);
  /* The rim of light along the top edge: the single most useful thing the
     app's surfaces do, and the reason its panels read as physical. */
  box-shadow: inset 0 1px 0 var(--zp-edge), 0 10px 28px -16px rgba(32,18,48,.45);
}
/* Nearest is most lit, furthest is deepest: the tonal step at the folds that
   §4 asks for, done with three solid colours rather than a gradient. */
.deck__back--1 { background: var(--zp-lit);  z-index: 3; transform: translate(calc(var(--fan) * .2), calc(var(--fan) * -.85)) rotate(2deg); }
.deck__back--2 { background: var(--zp);      z-index: 2; transform: translate(calc(var(--fan) * .4), calc(var(--fan) * -1.7)) rotate(4.1deg); }
.deck__back--3 { background: var(--zp-deep); z-index: 1; transform: translate(calc(var(--fan) * .6), calc(var(--fan) * -2.6)) rotate(6.2deg); }

/* The one automatic gesture on the page: the deck settles into its fan once,
   on load. It is a CSS animation rather than a JS class toggle so that it
   still runs, and still ENDS in the fanned state, with JavaScript disabled. */
@media (prefers-reduced-motion: no-preference) {
  .deck__back--1 { animation: settle1 .66s cubic-bezier(.2,.75,.3,1) .05s both; }
  .deck__back--2 { animation: settle2 .66s cubic-bezier(.2,.75,.3,1) .12s both; }
  .deck__back--3 { animation: settle3 .66s cubic-bezier(.2,.75,.3,1) .19s both; }
}
@keyframes settle1 {
  from { opacity: 0; transform: translate(0, 0) rotate(0deg); }
  to   { opacity: 1; transform: translate(calc(var(--fan) * .2), calc(var(--fan) * -.85)) rotate(2deg); }
}
@keyframes settle2 {
  from { opacity: 0; transform: translate(0, 0) rotate(0deg); }
  to   { opacity: 1; transform: translate(calc(var(--fan) * .4), calc(var(--fan) * -1.7)) rotate(4.1deg); }
}
@keyframes settle3 {
  from { opacity: 0; transform: translate(0, 0) rotate(0deg); }
  to   { opacity: 1; transform: translate(calc(var(--fan) * .6), calc(var(--fan) * -2.6)) rotate(6.2deg); }
}

/* ── the live card face ──────────────────────────────────────────────────
 * The app's own card anatomy, from study.jsx: centred, deck line, prompt, a
 * 32px hairline, answer in the accent. Set in the SANS — product UI is sans
 * everywhere; the serif belongs to the page's headings, not to the product.
 */
.zcard {
  position: relative; z-index: 4;
  background: var(--surface);
  border: 1px solid var(--rule-2);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-lg);
  padding: clamp(26px, 4vw, 38px) clamp(22px, 3.4vw, 34px);
  min-height: 304px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 14px;
  transition: transform .5s cubic-bezier(.22,.7,.28,1), box-shadow .5s ease;
}
.zcard__deck { font-size: 13px; font-weight: 500; letter-spacing: -0.005em; color: var(--ink-3); }
.zcard__q {
  font-family: var(--sans); font-size: clamp(22px, 3vw, 29px);
  line-height: 1.18; letter-spacing: -0.022em; font-weight: 600; color: var(--ink);
}
/* The hairline separates PROMPT from ANSWER, so before the reveal there is
   nothing for it to separate — unhidden it sat between the question and the
   Reveal button and read as a divider above a control. */
.zcard__rule { width: 32px; height: 1px; background: var(--rule); }
.zcard__a {
  font-family: var(--sans); font-size: clamp(19px, 2.4vw, 24px);
  line-height: 1.3; letter-spacing: -0.018em; font-weight: 600; color: var(--purple);
}
.zcard__note { font-size: 14px; line-height: 1.45; color: var(--ink-3); max-width: 36ch; }
/* ⚠️ PROGRESSIVE ENHANCEMENT, NOT DECORATION. Without scripting the answer is
   simply shown — the card reads as a finished flashcard, front and back —
   and the controls are absent, because a Reveal button that cannot reveal is
   worse than no button. JS hides the answer on init and takes over. */
.zcard__foot { display: none; margin-top: 6px; gap: 10px; flex-wrap: wrap; justify-content: center; }
.js .zcard__foot { display: flex; }
.zcard__rule { display: block; }
.js .zcard__rule { display: none; }
.js .zcard[data-flipped="true"] .zcard__rule { display: block; }
.zcard__grades { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }

/* Revealing lifts the card a little off its deck. That is the whole gesture:
   no dramatic 3D flip, which at this size reads as a widget demo. */
.zcard[data-flipped="true"] {
  transform: translateY(-5px) rotate(-.35deg);
  box-shadow: var(--shadow-lg), 0 34px 64px -34px rgba(32,18,48,.34);
}
/* Graded: the card goes back into the deck it came from, along the same
   diagonal the fan opens on, and the next one is dealt. */
.zcard { transition: transform .5s cubic-bezier(.22,.7,.28,1), box-shadow .5s ease, opacity .28s ease; }
.zcard.is-tucking {
  transform: translate(calc(var(--fan) * 1.2), calc(var(--fan) * -2.4)) rotate(4.6deg) scale(.975);
  opacity: 0;
  transition: transform .3s cubic-bezier(.4,0,.7,.2), opacity .3s ease;
}

.is-hidden { display: none !important; }

.zcard__a { animation: cardTurn .46s cubic-bezier(.2,.75,.3,1) both; transform-origin: top center; }
@keyframes cardTurn {
  from { opacity: 0; transform: rotateX(-32deg) translateY(-5px); }
  to   { opacity: 1; transform: none; }
}
.zcard__grades .btn { animation: liftIn .34s cubic-bezier(.2,.75,.3,1) both; }
.zcard__grades .btn:nth-child(2) { animation-delay: .05s; }
@keyframes liftIn {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}

[data-card-outcome] { animation: cardTurn .4s cubic-bezier(.2,.75,.3,1) both; }

@media (prefers-reduced-motion: reduce) {
  /* ⚠️ .deck__back is deliberately NOT in this list. Its transform is the fan
     itself — layout, not motion — and blanking it would collapse the three
     backs into one rectangle for exactly the readers who opted out of
     animation. The settle ANIMATION is already gated behind
     `prefers-reduced-motion: no-preference`, so nothing here has to undo it. */
  .zcard, .zcard__a, .zcard__grades .btn, [data-card-outcome] {
    animation: none !important; transition: none !important; transform: none !important;
  }
}

/* ── generic surfaces ────────────────────────────────────────────────────── */
.panel {
  background: #fff; border: 1px solid var(--rule-2); border-radius: var(--radius-lg);
  padding: clamp(22px, 3.4vw, 34px); box-shadow: none;
}
.panel--flat { background: #fff; }

.grid { display: grid; gap: clamp(16px, 2.4vw, 22px); }
@media (min-width: 720px) { .grid--2 { grid-template-columns: 1fr 1fr; } }
@media (min-width: 880px) { .grid--3 { grid-template-columns: repeat(3, 1fr); } }

.stack   { display: grid; gap: 16px; }
.stack-l { display: grid; gap: 26px; }

/* ── mistake / repair ────────────────────────────────────────────────────── */

/* ── pills / rows ────────────────────────────────────────────────────────── */
.rows { border-top: 1px solid var(--rule-2); }
.row { display: grid; gap: 4px; padding: 18px 0; border-bottom: 1px solid var(--rule-2); }
@media (min-width: 720px) { .row { grid-template-columns: 230px 1fr; gap: 24px; align-items: baseline; } }
.row__k { font-weight: 600; }
.row__v { color: var(--ink-2); }

/* ── capability list ─────────────────────────────────────────────────────── */
.caps { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
@media (min-width: 620px) { .caps { grid-template-columns: 1fr 1fr; } }
.caps li { display: flex; gap: 10px; align-items: flex-start; font-size: 16px; color: var(--ink-2); }
.caps svg { flex-shrink: 0; margin-top: 4px; color: var(--purple); }

/* ── the knowledge map: the one deep environment ──────────────────────────
 * ⚠️ CELESTIAL LANGUAGE BELONGS HERE AND NOWHERE ELSE ON THIS PAGE. The rest
 * of the site is stone white and ink; this is the single section allowed to
 * be sky, which is exactly what makes it land.
 *
 * The ground is the brand material at its deepest, lifted by ONE very soft
 * radial behind the figures. That radial is the only gradient on the site and
 * it is here because a flat rectangle of purple reads as a coloured box rather
 * than as depth — §33 permits tonal lighting inside this section specifically.
 *
 * Stars are warm cream and threads are the same cream at low alpha: those are
 * the app's own night values from scene.js (MARKS.night), not invented ones.
 * A cool-white star on a purple ground is the thing that would make this look
 * like a generic dark-mode hero.
 */
.sky {
  position: relative; isolation: isolate;
  background: var(--zp-deep);
  padding-block: clamp(60px, 8vw, 116px);
  margin-block: clamp(24px, 4vw, 56px);
  color: var(--zp-ink);
  overflow: clip;              /* clip, never hidden — hidden breaks sticky */
}
.sky::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: radial-gradient(112% 70% at 50% 42%, rgba(88,48,132,.44) 0%, rgba(40,18,64,.18) 46%, transparent 74%);
}
.sky h2 { color: var(--zp-ink); }
.eyebrow--onsky { color: var(--zp-ink-2); }
.lede--onsky { color: var(--zp-ink-2); max-width: 54ch; }
.sky__head { margin-bottom: clamp(26px, 3.6vw, 44px); }

/* Wider than the text column: the constellation is the hero of the section,
   so it is given the room the copy is not. */
.sky__map {
  width: min(1240px, calc(100% - var(--gutter)));
  margin: 0 auto;
}
.sky__map svg { display: block; width: 100%; height: auto; overflow: visible; }

.map .link { stroke: rgba(239,230,204,.22); stroke-width: 1; fill: none; }
.map .star { fill: #EFE6CC; opacity: var(--o, 1); }
/* Strength drives brightness and size, and nothing else — constellation.js is
   explicit that it is not a score and not a probability of recall. */
.map .star--established { fill: #FFF8E6; }
.map .star--strained    { fill: #E0C9B4; }
.map .star--dormant     { fill: #BFB39A; }

/* Motion is an ENHANCEMENT. Without JS the threads are drawn and the stars are
   lit; `.js` is set by an inline script on <html>, so the no-JS page shows the
   finished map rather than an empty rectangle. */
.js .map:not(.is-in) .link { stroke-dashoffset: var(--len); }
.js .map:not(.is-in) .star { opacity: 0; }
.js .map .link { stroke-dasharray: var(--len); }
.js .map.is-in .link { animation: drawLink 1.1s cubic-bezier(.4,.05,.2,1) both var(--d, 0ms); }
.js .map.is-in .star { animation: lightStar .5s ease-out both var(--d, 0ms); }
@keyframes drawLink { from { stroke-dashoffset: var(--len); } to { stroke-dashoffset: 0; } }
@keyframes lightStar { from { opacity: 0; transform: scale(.4); } to { opacity: var(--o, 1); transform: none; } }
.map .star { transform-box: fill-box; transform-origin: center; }

@media (prefers-reduced-motion: reduce) {
  .js .map .link { stroke-dashoffset: 0 !important; animation: none !important; }
  .js .map .star { opacity: var(--o, 1) !important; animation: none !important; }
}

/* The key. It is what makes the brightness mean something rather than look
   decorative, and every term on it is one of constellation.js's four states. */
.skykey {
  list-style: none; margin: clamp(26px, 3.4vw, 40px) 0 0; padding: 0;
  display: grid; gap: 10px 26px; font-size: 14px; color: var(--zp-ink-2);
}
@media (min-width: 640px) { .skykey { grid-template-columns: 1fr 1fr; } }
@media (min-width: 980px) { .skykey { grid-template-columns: repeat(4, 1fr); } }
.skykey li { display: flex; align-items: center; gap: 9px; }
.skykey b { color: var(--zp-ink); font-weight: 600; margin-right: 3px; }
.skykey__d { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.skykey__d--established { background: #FFF8E6; }
.skykey__d--emerging    { background: #EFE6CC; opacity: .55; }
.skykey__d--strained    { background: #E0C9B4; opacity: .42; }
.skykey__d--dormant     { background: #BFB39A; opacity: .22; }

.skyrows {
  margin-top: clamp(30px, 3.8vw, 46px);
  display: grid; gap: 16px 40px; font-size: 15px; line-height: 1.55; color: var(--zp-ink-2);
}
@media (min-width: 760px) { .skyrows { grid-template-columns: 1fr 1fr; } }
.skyrows b { display: block; color: var(--zp-ink); font-weight: 600; margin-bottom: 4px; }
.sky__note { margin-top: 22px; font-size: 14px; color: var(--zp-ink-2); opacity: .8; }

/* ── the card wheel ─────────────────────────────────────────────────────── */
/*
 * ⚠️ THIS IS THE APP'S OWN CARD AND THE APP'S OWN WHEEL, not a marketing
 * approximation of either.
 *
 * The card follows study.jsx: CENTRED, generous, a SHORT 32px hairline
 * between prompt and answer rather than a full-width border, the answer in
 * the accent colour, and prompt type that grows as the prompt gets shorter,
 * because a single word in the middle of an empty card has to read as the
 * thing you are being asked to recall and not as a caption.
 *
 * The motion follows WheelPicker.jsx: cards sit on a cylinder and every
 * channel is driven by signed distance from the centre line. Same clamped
 * curve, including the correction that cost an afternoon there, where the
 * sign has to be carried THROUGH the clamp or a far card rotates past ninety
 * degrees and turns to face away from the viewer.
 */
.wheelwrap { position: relative; margin-top: 30px; }

.wheel {
  list-style: none; margin: 0; padding: 0;
  height: 648px;                      /* the centre card, plus a slice of its neighbours */
  overflow-y: auto; overflow-x: hidden;
  overscroll-behavior-y: contain;     /* the page must not move when the deck ends */
  scroll-snap-type: y mandatory;
  -webkit-overflow-scrolling: touch;
  perspective: 1100px; perspective-origin: 50% 50%;
  outline: none;
  /* Cards enter and leave the frame rather than stopping against it. */
  mask-image: linear-gradient(180deg, transparent 0, #000 17%, #000 83%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 17%, #000 83%, transparent 100%);
}
.wheel::-webkit-scrollbar { width: 0; height: 0; }
.wheel { scrollbar-width: none; }
.wheel:focus-visible { outline: 2px solid var(--purple); outline-offset: 4px; border-radius: var(--radius-lg); }

/* Half a viewport of air at each end so the first and last card can reach the
   centre line like any other. `calc(50% - half a row)` resolves against the
   scroller's real height, which arithmetic on the constants did not. */
/* ⚠️ The longest card measured content == box exactly: no headroom, so one
   extra word would have spilled again. Rows carry ~30px of slack now. */
.wheel::before, .wheel::after { content: ""; display: block; height: calc(50% - (316px / 2)); }

.wheel__row {
  height: 316px;
  scroll-snap-align: center;
  display: flex; align-items: center; justify-content: center;
  padding: 0 clamp(6px, 3vw, 28px);
}

/* ⚠️ The snap area above carries NO transform. scroll-snap-align resolves
   against the TRANSFORMED box, so decorating the same element moves its own
   snap target: every card settles short of centre and none ever reaches
   full scale. The decoration lives on this child instead. */
.wheel__face {
  /* min-height, not height: the app's card sizes to its content
     (study.jsx uses minHeight: 288). A fixed height made every face
     fill its 316px row, so a two-line card was 60% empty air and the
     deck read as one unbroken column instead of separate cards. */
  width: 100%; max-width: 620px; min-height: 232px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; text-align: center;
  padding: 24px 32px;
  background: #fff;
  border: 1px solid var(--rule-2);
  border-radius: 22px;
  box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 18px 44px -26px rgba(0,0,0,.22);
  transform-style: preserve-3d; backface-visibility: hidden;
  will-change: transform, opacity;
}

.wheel__deck { font-size: 13px; color: var(--ink-3); letter-spacing: -0.005em; }
.wheel__q { font-weight: 600; letter-spacing: -0.022em; line-height: 1.16; color: var(--ink); }
.wheel__q--xl { font-size: clamp(28px, 4.2vw, 40px); }
.wheel__q--lg { font-size: clamp(21px, 2.8vw, 28px); }
.wheel__q--md { font-size: clamp(18px, 2.2vw, 22px); }
.wheel__q--sm { font-size: clamp(17px, 2vw, 20px); }
/* The short rule from the real card, not a full-width divider. */
.wheel__rule { display: block; width: 32px; height: 1px; background: var(--rule); }
.wheel__a { font-size: clamp(15px, 1.7vw, 17px); line-height: 1.45; color: var(--aubergine); font-weight: 550; max-width: 34ch; }
.wheel__q sub { font-size: .66em; }

.wheel__hint { margin-top: 14px; text-align: center; font-size: 14px; color: var(--ink-3); }

@media (max-width: 560px) {
  .wheel { height: 540px; }
  .wheel__row { height: 300px; }
  .wheel::before, .wheel::after { height: calc(50% - (300px / 2)); }
  .wheel__face { padding: 20px 18px; gap: 12px; min-height: 200px; }
}

/* ── comparison ──────────────────────────────────────────────────────────── */
.cmp { width: 100%; border-collapse: collapse; font-size: 16px; }
.cmp caption { text-align: left; color: var(--ink-3); font-size: 14px; padding-bottom: 14px; }
.cmp th, .cmp td { text-align: left; padding: 15px 14px; border-bottom: 1px solid var(--rule); vertical-align: top; }
.cmp thead th { font-size: 14px; letter-spacing: -0.006em; color: var(--ink-3); font-weight: 500; }
.cmp tbody th { font-weight: 500; color: var(--ink); }
.cmp td { color: var(--ink-2); }
.cmp .is-zynlo { color: var(--aubergine); font-weight: 550; }

/* ⚠️ A THREE-COLUMN COMPARISON CANNOT FIT A 320px PHONE, and putting it in a
   horizontal scroll box only hides that. Below 700px each row becomes its own
   block with its column name carried by the cell, so the comparison is still
   a comparison rather than a table you have to drag sideways. The markup stays
   a real <table>, so it is still announced as one. */
@media (max-width: 860px) {
  /* ⚠️ The caption must be switched too. Left as `display: table-caption`
     inside a table that is no longer a table, it lost its width basis and
     wrapped to roughly one word per line. */
  .cmp, .cmp caption, .cmp tbody, .cmp tr, .cmp th, .cmp td { display: block; width: 100%; }
  .cmp thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .cmp tr {
    border: 1px solid var(--rule); border-radius: var(--radius);
    background: rgba(255,255,255,.7); padding: 4px 16px 14px; margin-bottom: 12px;
  }
  .cmp tbody th { padding: 14px 0 10px; border-bottom: 1px solid var(--rule-2); font-weight: 650; }
  .cmp td { padding: 12px 0 0; border-bottom: 0; }
  .cmp td::before {
    content: attr(data-col); display: block;
    font-size: 13px; font-weight: 400; letter-spacing: -0.005em;
    color: var(--ink-3); margin-bottom: 4px;
  }
}

/* ── FAQ ─────────────────────────────────────────────────────────────────── */
.faq { border-top: 1px solid var(--rule-2); }
.faq details { border-bottom: 1px solid var(--rule-2); }
.faq summary {
  cursor: pointer; padding: 20px 40px 20px 0; position: relative;
  font-size: 17px; font-weight: 600; list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: ""; position: absolute; right: 8px; top: 50%; width: 10px; height: 10px;
  border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3);
  transform: translateY(-70%) rotate(45deg); transition: transform .2s ease;
}
.faq details[open] summary::after { transform: translateY(-30%) rotate(225deg); }
.faq .faq__a { padding: 0 0 22px; color: var(--ink-2); max-width: var(--measure); }
@media (prefers-reduced-motion: reduce) { .faq summary::after { transition: none; } }

/* ── final CTA ───────────────────────────────────────────────────────────── */
/* ⚠️ `clamp(40px, …)` as a floor meant a 320px screen spent 40px on the page
   gutter and 80px on this panel, leaving the heading 200px: three lines for
   six words. The floor now yields on small screens. */

/* ── footer ──────────────────────────────────────────────────────────────── */
.site-footer { border-top: 1px solid var(--rule-2); padding-block: 52px 40px; background: var(--paper-2); }
.footer-grid { display: grid; gap: 34px; }
@media (min-width: 720px) { .footer-grid { grid-template-columns: 1.4fr repeat(3, 1fr); } }
.footer-grid h4 { font-size: 14px; letter-spacing: -0.006em; color: var(--ink-3); margin-bottom: 14px; font-weight: 500; }
.footer-grid ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.footer-grid a { color: var(--ink-2); text-decoration: none; font-size: 15px; }
.footer-grid a:hover { color: var(--ink); text-decoration: underline; }
.footer-grid li[aria-disabled="true"] { color: var(--ink-3); font-size: 15px; }
.footer-legal { margin-top: 42px; padding-top: 24px; border-top: 1px solid var(--rule-2); color: var(--ink-3); font-size: 14px; }

/* ── legal / support document pages ──────────────────────────────────────── */
.doc { max-width: 720px; margin-inline: auto; padding-block: clamp(40px, 6vw, 72px); }
.doc h1 { font-size: clamp(32px, 5vw, 46px); }
.doc .meta { color: var(--ink-3); font-size: 15px; margin-top: 12px; }
.doc h2 { font-size: clamp(20px, 2.6vw, 25px); margin-top: 42px; }
.doc h3 { margin-top: 28px; }
.doc p, .doc li { color: var(--ink-2); }
.doc p { margin-top: 14px; }
.doc ul, .doc ol { margin-top: 14px; padding-left: 22px; display: grid; gap: 8px; }
.doc hr { border: 0; border-top: 1px solid var(--rule-2); margin: 36px 0; }
.doc .callout {
  margin-top: 20px; padding: 18px 20px; border-radius: var(--radius);
  background: var(--paper-2); border: 1px solid var(--rule-2);
}
.doc .callout--draft { background: var(--purple-soft); border-color: var(--purple-line); }
.doc .callout p:first-child { margin-top: 0; }
.doc a { color: var(--aubergine); }

/* ── reveal on scroll, as pure enhancement ───────────────────────────────── */
/* ⚠️ Gated on .js: with scripting off, nothing is ever hidden. A page that
   needs JavaScript to show its own words is not a marketing page. */
.js .reveal {
  opacity: 0; transform: translateY(22px);
  transition: opacity .62s cubic-bezier(.22,.7,.28,1), transform .62s cubic-bezier(.22,.7,.28,1);
}
.js .reveal.is-in { opacity: 1; transform: none; }

/* A stagger inside a revealed block, so a list arrives in order rather than
   as one slab. The delay is set from JS as a custom property, because the
   number of children is not known here. */
.js .stagger > * { opacity: 0; transform: translateY(16px); }
.js .stagger.is-in > * {
  opacity: 1; transform: none;
  transition: opacity .52s cubic-bezier(.22,.7,.28,1) var(--d, 0ms),
              transform .52s cubic-bezier(.22,.7,.28,1) var(--d, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  .js .reveal, .js .stagger > *, .js .stagger.is-in > * {
    opacity: 1; transform: none; transition: none;
  }
}

.vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── the five states ──────────────────────────────────────────────────────
 * ONE knowledge object, drawn five times. Every step is a miniature of the
 * real card so the row reads as a thing changing rather than as a feature
 * list, and the copy under each is one sentence: the benefit, not the
 * mechanism.
 *
 * No arrows. A single hairline runs behind the step numbers and does the
 * work five chevrons would have done more loudly.
 */
.eras {
  list-style: none; margin: clamp(36px, 5vw, 56px) 0 0; padding: 0;
  display: grid; gap: clamp(26px, 3vw, 30px);
}
.era { display: grid; gap: 12px; align-content: start; }
.era__n {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.1em;
  color: var(--ink-3); position: relative;
}
.era__t { font-size: 17px; letter-spacing: -0.014em; }
.era__d { font-size: 14.5px; line-height: 1.5; color: var(--ink-2); }

/* The miniature. Same material as the real card — white surface on the stone
   ground, the same 22px corner — at a size where one glance is the point. */
.era__card {
  background: var(--surface); border: 1px solid var(--rule-2);
  border-radius: var(--radius-card); box-shadow: var(--shadow);
  min-height: 116px; padding: 18px 16px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; text-align: center;
}
.era__prompt { font-size: 14px; font-weight: 550; line-height: 1.3; color: var(--ink); }
.era__pill {
  font-size: 12px; font-weight: 600; color: #fff; background: var(--zp);
  border-radius: 999px; padding: 5px 13px;
}
/* The typed state: a field with a caret, which is what production looks like. */
.era__field {
  font-size: 13px; color: var(--ink-2); background: var(--paper-2);
  border: 1px solid var(--rule-2); border-radius: 8px;
  padding: 6px 10px; display: inline-flex; align-items: center; gap: 2px;
}
.era__caret { display: inline-block; width: 1.5px; height: 13px; background: var(--purple); }
@media (prefers-reduced-motion: no-preference) {
  .era__caret { animation: caret 1.1s steps(1) infinite; }
  @keyframes caret { 0%, 55% { opacity: 1 } 56%, 100% { opacity: 0 } }
}
/* Wrong and right, muted. §17's rule applies here too: no traffic-light red
   and green — a miss is information, not an alarm. */
.era__wrong {
  font-size: 13.5px; color: var(--ink-3); text-decoration: line-through;
  text-decoration-color: var(--rule); text-decoration-thickness: 1px;
}
.era__right { font-size: 13.5px; font-weight: 600; color: var(--purple); }
.era__chip {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em;
  color: var(--ink-3); background: var(--paper-2);
  border: 1px solid var(--rule-2); border-radius: 999px; padding: 4px 10px;
}

@media (min-width: 760px) {
  .eras {
    --era-gap: clamp(14px, 1.6vw, 20px);
    grid-template-columns: repeat(5, 1fr); gap: var(--era-gap);
  }
  /* The hairline that replaces five chevrons. It spans from just past the
     step number to the NEXT step's number, reaching across the grid gap with
     a negative offset.
     ⚠️ Not `width: 100vw` clipped by `overflow: hidden` on the row. That is
     the same trick that broke position:sticky elsewhere on this page —
     hidden on one axis silently makes the element a scroll container on the
     other — and it is not needed: an inset that ends where the next column
     begins is exact, and the last step simply omits it. */
  .era { position: relative; }
  .era::after {
    content: ""; position: absolute; top: 8px;
    left: 27px; right: calc(-1 * var(--era-gap));
    height: 1px; background: var(--rule-2);
  }
  .era:last-child::after { display: none; }
}

/* ── the repair ───────────────────────────────────────────────────────────
 * Two lines, one word apart. The concept has to land before the paragraph
 * beside it is read, so the only colour in the figure is on the word that
 * changed — and the wrong line is MUTED rather than red. A miss here is
 * information, not an alarm, which is the whole claim the section makes.
 */
.repair { margin: 0; display: grid; gap: 6px; justify-items: start; }
.repair__k {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3); margin-top: 6px;
}
.repair__line {
  font-size: clamp(21px, 2.7vw, 30px); line-height: 1.25; letter-spacing: -0.02em;
  font-weight: 600;
}
.repair__line--was { color: var(--ink-3); }
.repair__line--is  { color: var(--ink); }
/* The changed portion. Underlined rather than highlighted: a filled mark at
   this size reads as a search hit. */
.repair__d {
  text-decoration: underline; text-decoration-thickness: 1px;
  text-underline-offset: 4px; text-decoration-color: var(--rule);
}
.repair__d--is { color: var(--purple); text-decoration-color: var(--purple-line); }
/* The turn between the two lines: a short vertical hairline, not an arrow. */
.repair__turn {
  display: block; width: 1px; height: 26px; background: var(--rule);
  margin: 10px 0 4px 3px;
}
.repair__why {
  margin-top: 18px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 14px; color: var(--ink-2);
}
.repair__why i { font-style: normal; color: var(--ink-3); }
.repair__why span:last-child { color: var(--ink); font-weight: 600; }

/* ── the workspace ────────────────────────────────────────────────────────
 * The product, at size. §18 asks someone who knows Anki or Mochi to look at
 * this and believe Zynlo is a serious manager, so it is a RECONSTRUCTION of
 * the real screen rather than a diagram: the controls, their order, the
 * placeholder text and the row metadata all come from Workspace.jsx,
 * views.js and labels.js.
 *
 * Built in HTML rather than dropped in as a PNG. It stays sharp at any
 * density, it re-flows on a phone instead of becoming an unreadable
 * thumbnail, and it carries one honest alt description for screen readers
 * instead of pretending to be interactive.
 */
.wsstage {
  position: relative;
  margin: clamp(30px, 4.4vw, 52px) auto clamp(26px, 3.4vw, 40px);
  max-width: min(1160px, calc(100% - var(--gutter) * 2));
}
.ws {
  background: var(--surface);
  border: 1px solid var(--rule-2);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: clamp(16px, 2.1vw, 26px);
  display: grid; gap: clamp(12px, 1.4vw, 16px);
  font-size: 14px; color: var(--ink);
}
.ws__bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.ws__name { font-size: clamp(15px, 1.7vw, 18px); font-weight: 650; letter-spacing: -0.016em; }
.ws__sub { font-size: 13px; color: var(--ink-3); margin-top: 2px; }
.ws__more { display: flex; gap: 3px; padding-top: 8px; }
.ws__more i { width: 3px; height: 3px; border-radius: 50%; background: var(--ink-3); }

.ws__actions { display: flex; gap: 8px; flex-wrap: wrap; }
.ws__btn {
  font-size: 13.5px; font-weight: 600; letter-spacing: -0.008em;
  padding: 8px 15px; border-radius: 999px;
  border: 1px solid var(--rule); color: var(--ink);
}
.ws__btn--go { background: var(--zp); border-color: var(--zp); color: #fff; }

.ws__search {
  display: flex; align-items: center; gap: 9px;
  background: var(--paper-2); border: 1px solid var(--rule-2);
  border-radius: 10px; padding: 10px 13px; color: var(--ink-3); font-size: 13.5px;
}

.ws__chips { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.ws__chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 500; color: var(--ink-2);
  border: 1px solid var(--rule-2); border-radius: 999px; padding: 6px 12px;
}
.ws__chip--on { background: var(--purple-soft); border-color: var(--purple-line); color: var(--purple); }
.ws__grow { flex: 1 1 auto; }
.ws__icon { color: var(--ink-3); display: inline-flex; padding: 4px; }

.ws__rows { list-style: none; margin: 0; padding: 0; display: grid; }
/* ⚠️ THE ROW STACKS ON A PHONE. Side by side, the question and its metadata
   split a 320px row between them and the question wrapped to six characters
   a line. CardRow in the app puts Meta underneath for the same reason, so
   this matches the product as well as fixing the squash. */
.ws__row {
  display: grid; gap: 3px;
  padding: 12px 2px; border-top: 1px solid var(--rule-2);
}
@media (min-width: 560px) {
  .ws__row {
    display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  }
}
.ws__row:first-child { border-top: 0; }
.ws__q { font-size: 14px; font-weight: 500; letter-spacing: -0.008em; min-width: 0; }
.ws__meta {
  font-size: 12.5px; color: var(--ink-3); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* Due-now and overdue are the app's one piece of colour in this list. */
.ws__meta b { font-weight: 600; color: var(--purple); }

.ws__foot {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  border-top: 1px solid var(--rule-2); padding-top: 14px;
  font-size: 12.5px; color: var(--ink-3);
}
.ws__stat b {
  font-family: var(--mono); font-size: 15px; font-weight: 600; color: var(--ink);
  margin-right: 5px; font-variant-numeric: tabular-nums;
}
.ws__new { font-size: 13px; font-weight: 600; color: var(--ink); }

/* The phone, layered at the right edge. It only appears where there is real
   room beside the list; below that it would sit ON the rows it is meant to
   accompany, so it is removed rather than shrunk. */
.wsphone { display: none; }
/* ⚠️ ABSOLUTE POSITIONING PUT THIS ON TOP OF THE LIST. Anchored to the
   stage's bottom-right it sat over rows four to six and hid the very
   metadata — stage and due date — that the section exists to show. It is a
   COLUMN now, not an overlay: the stage becomes a two-track grid and the
   phone leans into the panel by 18px, which is less than the panel's own
   26px of padding, so it overlaps the material and never the text. */
@media (min-width: 1240px) {
  .wsstage {
    display: grid; grid-template-columns: minmax(0, 1fr) 196px;
    gap: 26px; align-items: center;
  }
  .wsphone {
    display: grid; gap: 8px; position: relative; z-index: 2;
    margin-left: -18px; width: 196px;
    background: var(--surface); border: 1px solid var(--rule-2);
    border-radius: 20px; padding: 16px 14px;
    box-shadow: 0 2px 6px rgba(32,18,48,.06), 0 30px 60px -26px rgba(32,18,48,.30);
    transform: rotate(-1.4deg);
  }
  .wsphone__t { font-size: 14px; font-weight: 650; letter-spacing: -0.014em; }
  .wsphone__card {
    display: flex; align-items: baseline; gap: 7px;
    background: var(--paper-2); border-radius: 10px; padding: 9px 11px;
  }
  .wsphone__card b { font-family: var(--mono); font-size: 17px; font-weight: 600; }
  .wsphone__card span { font-size: 12px; color: var(--ink-3); }
  .wsphone__row {
    display: flex; align-items: center; gap: 8px;
    font-size: 12px; color: var(--ink-2); padding: 3px 1px;
  }
  .wsphone__row i { width: 6px; height: 6px; border-radius: 50%; background: var(--zp-lit); }
}

/* The capability list, cut from eight rows to four and set inline: the
   product visual above now makes the argument the ticks used to make. */
.caps--tight { display: grid; gap: 9px; margin-top: 4px; }
.caps--tight li { display: block; font-size: 14.5px; color: var(--ink-2); padding-left: 16px; position: relative; }
.caps--tight li::before {
  content: ""; position: absolute; left: 0; top: 10px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--rule);
}
@media (min-width: 760px) { .caps--tight { grid-template-columns: 1fr 1fr; gap: 9px 30px; } }

/* ── the widening interval ────────────────────────────────────────────────
 * Four rows against one axis. The only claim made is the SHAPE — as recall
 * holds, the same card lands further out — which is what a reader needs and
 * is all the homepage should say. No replay, no log, no parameters.
 * Dot positions are the real ratios against the 31-day row (3/31, 11/31),
 * so the picture is not lying about the spacing it depicts.
 */
.ivl { list-style: none; margin: 0; padding: 0; display: grid; gap: 18px; }
.ivl__row { display: grid; grid-template-columns: 74px minmax(0, 1fr); gap: 16px; align-items: center; }
.ivl__k {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-3); font-variant-numeric: tabular-nums;
}
.ivl__track { position: relative; height: 13px; }
.ivl__track::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: 1px; background: var(--rule-2);
}
.ivl__dot {
  position: absolute; top: 50%; left: var(--at);
  width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
  border-radius: 50%; background: var(--zp-lit);
}
/* The furthest card is the settled one, so it is the most present. */
.ivl__dot--far { width: 12px; height: 12px; margin: -6px 0 0 -6px; background: var(--zp); }

@media (prefers-reduced-motion: no-preference) {
  .ivl.is-in .ivl__dot { animation: slideOut .6s cubic-bezier(.2,.75,.3,1) both; }
  .ivl.is-in .ivl__row:nth-child(2) .ivl__dot { animation-delay: .08s; }
  .ivl.is-in .ivl__row:nth-child(3) .ivl__dot { animation-delay: .16s; }
  .ivl.is-in .ivl__row:nth-child(4) .ivl__dot { animation-delay: .24s; }
  @keyframes slideOut { from { opacity: 0; transform: translateX(-14px); } to { opacity: 1; transform: none; } }
}

/* ── confusion pairs ──────────────────────────────────────────────────────
 * Two things that keep swapping places, so they are drawn as a pair with the
 * swap between them rather than as four loose tags.
 */
.confs { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.conf {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--rule-2);
  border-radius: 999px; padding: 11px 18px;
  font-size: 15px; letter-spacing: -0.01em;
}
.conf__a { text-align: right; color: var(--ink-2); min-width: 0; overflow-wrap: anywhere; }
.conf__b { text-align: left; color: var(--ink); font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.conf__x { font-style: normal; color: var(--ink-3); font-size: 13px; }
@media (min-width: 620px) { .confs { grid-template-columns: 1fr 1fr; gap: 10px 12px; } }

/* ── three ways to study ──────────────────────────────────────────────────
 * Deliberately small. It is a fact the reader needs once; the sections
 * around it are the ones doing the arguing.
 */
.section--tight { padding-block: clamp(30px, 4vw, 48px); }
.modes { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px 34px; }
@media (min-width: 720px) { .modes { grid-template-columns: repeat(3, 1fr); } }
.mode { padding: 18px 0; border-top: 1px solid var(--rule-2); }
.mode h3 { font-size: 16px; letter-spacing: -0.012em; }
.mode p { margin-top: 5px; font-size: 14.5px; color: var(--ink-2); }

/* ── the import flow ──────────────────────────────────────────────────────
 * One line, five stops — not five panels. The architecture claim underneath
 * is the point of the section: the model is not ours and does not run here.
 */
.flow {
  list-style: none; margin: clamp(30px, 4vw, 46px) 0 0; padding: 0;
  display: grid; gap: 22px;
}
.flow__s { position: relative; padding-top: 20px; }
/* The stop: a dot on a hairline that runs to the next one. */
.flow__s::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--zp-lit); z-index: 1;
}
.flow__s b { display: block; font-size: 15px; font-weight: 650; letter-spacing: -0.012em; }
.flow__s span { display: block; margin-top: 4px; font-size: 14px; line-height: 1.5; color: var(--ink-2); }
@media (min-width: 860px) {
  .flow { grid-template-columns: repeat(5, 1fr); gap: 0 20px; }
  .flow__s::after {
    content: ""; position: absolute; top: 4px; left: 9px; right: -20px;
    height: 1px; background: var(--rule-2);
  }
  .flow__s:last-child::after { display: none; }
}
.flow__truth {
  margin-top: clamp(26px, 3.4vw, 38px);
  font-size: clamp(16px, 1.9vw, 19px); letter-spacing: -0.014em;
  color: var(--ink); font-weight: 550; max-width: 46ch;
}

/* ── the closing surface ──────────────────────────────────────────────────
 * The deck from the hero, closing. Three card edges emerge from the corners
 * in the same material; the page's card-layer story opens and shuts with
 * the same object. Restrained on purpose — this is not a second hero.
 */
.close {
  position: relative; isolation: isolate; overflow: clip;
  background: var(--zp);
  color: var(--zp-ink);
  padding-block: clamp(64px, 9vw, 122px);
  margin-top: clamp(30px, 5vw, 64px);
}
.close__in { position: relative; z-index: 1; }
.close h2 { color: var(--zp-ink); }
.close p {
  margin-top: 20px; max-width: 46ch;
  font-size: clamp(16px, 1.9vw, 18px); line-height: 1.55; color: var(--zp-ink-2);
}
.close__cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; align-items: center; }

/* Card edges. They are BEHIND the copy (z-index -1 on an isolated stacking
   context) and clipped by the section, so they read as cards passing under
   the surface rather than as floating shapes. */
.close__e {
  position: absolute; z-index: -1;
  width: clamp(200px, 30vw, 360px); aspect-ratio: 1.42;
  border-radius: var(--radius-card);
  background: var(--zp-lit);
  box-shadow: inset 0 1px 0 var(--zp-edge);
}
.close__e--1 { right: -8%;  top: -16%;  transform: rotate(-9deg);  background: var(--zp-lit); }
.close__e--2 { right: 4%;   bottom: -26%; transform: rotate(6deg);  background: var(--zp-deep); }
.close__e--3 { left: -13%;  bottom: -30%; transform: rotate(-5deg); background: var(--zp-deep); opacity: .7; }

/* Buttons on the material. The primary inverts to paper; a dark-on-dark
   button would disappear into the surface it sits on. */
.btn--onzp { background: var(--paper); color: var(--zp); border-color: var(--paper); }
.btn--onzp:hover { background: #fff; border-color: #fff; }
.btn--onzp-ghost { background: transparent; color: var(--zp-ink); border-color: rgba(255,255,255,.28); }
.btn--onzp-ghost:hover { border-color: rgba(255,255,255,.5); background: rgba(255,255,255,.06); }
.btn--onzp-ghost .btn__note { color: var(--zp-ink-2); }

/* ── rhythm ───────────────────────────────────────────────────────────────
 * Scheduling and recurring mistakes were the same shape twice: copy left,
 * figure right, one after the other. Flipping the second is the cheapest
 * way to stop two adjacent sections reading as one template.
 */
@media (min-width: 720px) {
  .grid--flip > :first-child { order: 2; }
  .grid--flip > :last-child  { order: 1; }
}

/* ⚠️ A DISABLED BUTTON ON THE MATERIAL NEEDS ITS OWN COLOUR. The base rule
   paints an unavailable button in --ink-3, which is tuned for the stone
   ground; on the closing purple that measured 2.99:1 and the label was
   barely legible. The old dark CTA had an override, but it was scoped to
   `.final` and renaming the section silently dropped it -- the button went
   back to dark-on-dark with nothing failing loudly. */
.close .btn[aria-disabled="true"] {
  color: var(--zp-ink-2); border-color: rgba(255,255,255,.22); background: transparent;
}
.close .btn__note { color: var(--zp-ink-2); }
