/* Etude — front page.
   Top to bottom: demo video, headline, the Local/Private tree (drawn as you
   scroll), the download, the news strip. */

.home {
  position: relative;
  /* the tree's two columns — the labels row and the node row share them so
     the lines between the two run straight */
  --col-gap: clamp(20px, 9vw, 150px);
  --col: min(390px, calc((100vw - 2 * var(--gutter) - var(--col-gap)) / 2));
}

/* ---- the lines ------------------------------------------------------------
   One SVG laid over the whole of <main>. home.js fills it. */
.wire {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
  animation: fade-in 0.9s ease 0.35s both;
}
.wire path {
  fill: none;
  stroke: var(--wire);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 0.5s ease, stroke-width 0.5s ease;
}
/* Colour tells the two branches apart. At rest the page is neutral; as soon
   as you start to scroll, the "Local" label and its line turn blue and "Private" and
   its line turn green. Where the two run together each fades into ink, and
   the single line from there on is ink and a little heavier. */
.tree-live .wire .w-local { stroke: rgb(var(--net)); stroke-width: 2; }
.tree-live .wire .w-priv  { stroke: rgb(var(--priv)); stroke-width: 2; }
.wire .w-joinL { stroke: url(#wireFadeL); stroke-width: 2; }
.wire .w-joinR { stroke: url(#wireFadeR); stroke-width: 2; }
.wire .w-trunk { stroke: var(--ink); stroke-width: 2; }
.wire .stop-local { stop-color: rgb(var(--net)); }
.wire .stop-priv  { stop-color: rgb(var(--priv)); }
.wire .stop-ink   { stop-color: var(--ink); }
.root-label { transition: color 0.5s ease; }
.tree-live #labelLocal   { color: rgb(var(--net)); }
.tree-live #labelPrivate { color: rgb(var(--priv)); }
/* Before the first scroll the two arrowheads nod, as a cue that there is
   more below. The nod is on the inner path: the group carries the position. */
.wire.cue .wire-head path { animation: head-nod 1.9s ease-in-out infinite; }
@keyframes head-nod {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(5px); }
}

/* ---- hero -----------------------------------------------------------------
   The first screen, centred in the window: the app icon, the headline, the
   downloads, then the two roots with their arrows hanging under them. The
   demo is further down, in the tree (.showcase). */
.hero {
  --band: clamp(60px, 11vh, 120px);   /* open space under the labels, where the first arrows hang */
  position: relative;
  z-index: 1;
  min-height: min(calc(100vh - var(--topbar-h)), 1080px);
  min-height: min(calc(100svh - var(--topbar-h)), 1080px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: clamp(14px, 3vh, 34px) var(--gutter) var(--band);
}
.hero-icon {
  display: block;
  flex: none;
  width: clamp(84px, 9vw, 120px);
  height: auto;
  aspect-ratio: 1;
  margin-bottom: clamp(18px, 3vh, 30px);
  animation: rise-in 0.9s var(--ease) 0.05s both;
}
/* the app icon is a white square: a soft edge sets it off the light page;
   on the dark one it carries itself */
.hero-icon, .logo-slot {
  filter: drop-shadow(0 0 0.5px rgba(11, 11, 13, 0.22)) drop-shadow(0 10px 26px rgba(11, 11, 13, 0.14));
}
:root[data-theme="dark"] .hero-icon,
:root[data-theme="dark"] .logo-slot { filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.55)); }
.demo {
  aspect-ratio: 16 / 10;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: clamp(10px, 1.2vw, 16px);
  border: 1px solid var(--line);
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(11, 11, 13, 0.05),
              0 24px 60px rgba(11, 11, 13, 0.10);
}
:root[data-theme="dark"] .demo {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6), 0 24px 70px rgba(0, 0, 0, 0.55);
}
/* "The AI for what you'd never share with ChatGPT.", on two balanced lines */
.headline {
  flex: none;
  position: relative;
  max-width: 13em;
  font-size: clamp(34px, 4.7vw, 70px);
  font-weight: 700;
  line-height: 1.04;
  letter-spacing: -0.032em;
  text-align: center;
  text-wrap: balance;
  color: var(--ink);
  /* fades only, no travel: home.js measures the hero to place the lines */
  animation: fade-in 0.9s ease 0.15s both;
}
.nb { white-space: nowrap; }
@keyframes rise-in {
  0%   { opacity: 0; transform: translateY(10px); }
  100% { opacity: 1; transform: none; }
}
@keyframes fade-in {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}
@media (max-width: 639.98px) {
  .headline { font-size: clamp(34px, 10.4vw, 50px); }
}

/* ---- the line under the headline ------------------------------------------
   "Easy setup in a few clicks: No terminal. …" */
.statement {
  flex: none;
  max-width: 44em;
  margin-top: clamp(14px, 2.4vh, 24px);
  font-size: clamp(17px, 1.6vw, 22px);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -0.014em;
  text-align: center;
  text-wrap: balance;
  color: var(--ink-2);
  animation: fade-in 0.9s ease 0.15s both;
}

/* when the line has to wrap, it breaks after the colon, not inside the list */
.statement-list { display: inline-block; }
/* "Completely Free.", last in the list: in ink, linked to why it's free on
   Why Etude (why.html#free) */
.free-link {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--glass-rim-hover);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 0.2s ease;
}
.free-link:hover { text-decoration-color: var(--ink); }

/* ---- the downloads ----------------------------------------------------------
   Under the headline, and again at the foot of the tree. */
.hero-cta {
  flex: none;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: clamp(30px, 5.5vh, 60px);
  animation: fade-in 0.9s ease 0.2s both;
}
.hero-cta .cta-button { font-size: 17px; padding: 13px 34px; }
.cta-row { display: flex; justify-content: center; }
.hero-cta .cta-note { margin-top: 12px; font-size: 13px; }

/* the soft requirement: it runs on less, but this is what it is made for */

/* ---- the two roots --------------------------------------------------------
   The headline's two words again, as the heads of the tree's two columns. */
.roots {
  flex: none;
  display: grid;
  grid-template-columns: repeat(2, var(--col));
  column-gap: var(--col-gap);
  justify-content: center;
  margin-top: clamp(30px, 5.5vh, 60px);
  animation: fade-in 0.9s ease 0.25s both;
}
/* the first place the page says "Local" and "Private": big, and blue and
   green once you scroll */
.root-label {
  justify-self: center;
  font-size: clamp(30px, 3.5vw, 52px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--ink);
}
/* A window too short to hold the whole first screen: the hero is just as
   tall as its contents and the page scrolls. */
@media (max-height: 560px) {
  .hero { min-height: 0; }
}

/* ---- the demo -------------------------------------------------------------
   In the tree, between "You own your AI." and the download: as large as the
   page allows, never taller than the window. */
.showcase {
  position: relative;
  z-index: 1;
  padding: clamp(104px, 15vh, 150px) var(--gutter) 0;
}
.showcase-media { display: flex; justify-content: center; }
.showcase .demo { width: min(100%, 1104px, calc((100svh - var(--topbar-h) - 80px) * 1.6)); }
@media (max-width: 639.98px) {
  .showcase .demo.app-demo { width: min(100%, 560px); }
}

/* ---- the icon's notes -----------------------------------------------------
   Hovering the app icon scatters music notes round it, in ink. The wrapper's
   font-size is the icon's width, so the notes' em offsets are in icon widths
   and the cloud scales with the icon. */
.hero-spark {
  position: relative;
  display: block;
  flex: none;
  font-size: clamp(84px, 9vw, 120px);
  margin-bottom: clamp(18px, 3vh, 30px);
  cursor: default;
}
.hero-spark .hero-icon { width: 1em; margin-bottom: 0; }
.note-field {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  pointer-events: none;
  letter-spacing: 0;
}
.note {
  position: absolute;
  left: 0;
  top: 0;
  color: var(--ink);   /* ink: black, or white in dark */
  opacity: 0;
  /* parked at the centre, scaled down — they grow out of the icon */
  transform: translate3d(-50%, -50%, 0) scale(0.3) rotate(0deg);
  /* Retracting: short and accelerating, no bounce. A spring on the way back
     pushes the notes through the centre before they settle. */
  transition: transform 0.3s cubic-bezier(0.45, 0, 0.75, 0.15),
              opacity 0.19s ease-in;
  will-change: transform, opacity;
}
.notes-on .note {
  /* Expanding — the spring belongs on the way out only. */
  transition: transform 0.62s cubic-bezier(0.18, 0.9, 0.28, 1.25),
              opacity 0.34s ease;
  opacity: var(--o, 0.9);
  transform: translate3d(calc(-50% + var(--dx)), calc(-50% + var(--dy)), 0)
             rotate(var(--r, 0deg));
}
.note-inner {
  display: block;
  width: var(--w, 0.4em);
  height: var(--w, 0.4em);
  /* runs unconditionally: gated on .notes-on it snaps to zero the instant
     the class drops and yanks every note sideways mid-retract */
  animation: note-drift 2.9s ease-in-out infinite;
  animation-delay: var(--d, 0s);
}
.note svg { width: 100%; height: 100%; }
@keyframes note-drift {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-7px) rotate(4deg); }
}
/* a phone has no hover to trigger them */
@media (max-width: 639.98px) { .note-field { display: none; } }

/* ---- the tree ------------------------------------------------------------ */
.tree {
  position: relative;
  z-index: 1;
  padding: clamp(74px, 13vh, 150px) var(--gutter) 0;
  text-align: center;
}
.pair {
  display: grid;
  grid-template-columns: repeat(2, var(--col));
  column-gap: var(--col-gap);
  justify-content: center;
  align-items: start;
}
/* The two nodes are staggered: Private sits lower, so its arrow is longer
   and the Local node is reached, and read, first. home.js follows wherever
   the nodes are, so this one number sets the stagger. */
#nodePrivate { margin-top: clamp(120px, 20vh, 220px); }
@media (max-width: 639.98px) { #nodePrivate { margin-top: clamp(90px, 14vh, 150px); } }
.node p {
  font-size: clamp(17px, 1.65vw, 23px);
  line-height: 1.36;
  letter-spacing: -0.012em;
  color: var(--ink-2);
  text-wrap: balance;
}
/* grey sentence, the phrase that matters in ink */
.node b { font-weight: 500; color: var(--ink); }

.node-merge { max-width: 640px; margin: clamp(140px, 21vh, 200px) auto 0; }
.node-merge p { font-size: clamp(20px, 2.1vw, 29px); line-height: 1.3; letter-spacing: -0.018em; }
.node-merge b { font-weight: 600; }
/* each sentence is its own block, so each is balanced on its own and a
   narrow screen never strands one word on the last line */
.node .ln { display: block; text-wrap: balance; }
/* "Nothing": lifted out of its grey sentence, in ink and italic */
.node-merge em { color: var(--ink); font-weight: 500; }

.node-own { margin-top: clamp(104px, 15vh, 150px); }
.node-own p {
  font-size: clamp(40px, 5.6vw, 78px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.035em;
  color: var(--ink);
}
/* "own": the one underline on the page. It runs from the Local blue into the
   Private green — the two branches, arriving together. It is drawn as a
   background so it can run out from the left as the line lands, and back. */
.mine {
  background: linear-gradient(90deg, rgb(var(--net)), rgb(var(--priv))) 0 100% / 100% 0.07em no-repeat;
  padding-bottom: 0.06em;
}
.js .mine { background-size: 0 0.07em; transition: background-size 0.3s ease; }
.js .is-in .mine { background-size: 100% 0.07em; transition: background-size 0.6s var(--ease) 0.4s; }

/* ---- node pictures -------------------------------------------------------
   Line drawings built from SVG primitives; nothing is an image file. Grey
   for hardware; blue and green pick up the Local and Private branches. */
.glyph {
  display: flex;
  justify-content: center;
  margin-bottom: clamp(14px, 2.2vh, 22px);
  color: var(--ink-3);
}
.glyph svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  overflow: visible;
}
.glyph-wide svg { width: min(468px, 100%); height: auto; }
.glyph .accent { stroke: rgb(var(--net)); }
.glyph .accent-fill { fill: rgb(var(--net)); stroke: none; }
.glyph .accent.priv { stroke: rgb(var(--priv)); }
.glyph .accent-fill.priv { fill: rgb(var(--priv)); }
.glyph .mask { fill: var(--bg); stroke: none; }
.glyph .dash { stroke-dasharray: 2 6; }
.glyph .faint { opacity: 0.6; }
.glyph .link { stroke-dasharray: 1.5 5; opacity: 0.7; }
/* packets shuttle between the model and the conversation, inside the computer */
.glyph .shuttle { animation: shuttle 2.8s cubic-bezier(0.5, 0, 0.5, 1) infinite; }
.glyph .shuttle.back { animation-name: shuttle-back; animation-delay: 0.5s; }
@keyframes shuttle {
  0%   { transform: translateX(0);    opacity: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translateX(18px); opacity: 0; }
}
@keyframes shuttle-back {
  0%   { transform: translateX(0);     opacity: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translateX(-18px); opacity: 0; }
}
/* The cross draws itself, one stroke then the other, as the node arrives —
   unhurried, so it reads as being drawn rather than appearing — and undraws
   on the way back up. */
.glyph .strike { stroke-dasharray: 36; stroke-dashoffset: 0; }
.js .glyph .strike { stroke-dashoffset: 36; transition: stroke-dashoffset 0.35s ease; }
.js .is-in .glyph .strike { stroke-dashoffset: 0; transition: stroke-dashoffset 0.72s cubic-bezier(0.45, 0, 0.3, 1) 0.52s; }
.js .is-in .glyph .strike.second { transition-delay: 1s; }
.no-anim .glyph .strike { transition: none !important; }
.glyph .cut { stroke: var(--ink); stroke-width: 2.2; }
.glyph .ghost { opacity: 0.55; stroke-dasharray: 3 4.5; }
.glyph .core { animation: core-pulse 2.6s ease-in-out infinite; }
@keyframes core-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

/* ---- reveal ---------------------------------------------------------------
   A node comes into focus as its line lands on it, and goes back out if you
   scroll up past that point — the page plays the same in both directions.
   The hidden state only applies when JS is running (html.js); without it the
   page is simply all there. The transform is on the inner wrapper so the
   node's own box stays put for home.js to measure. */
.js [data-reveal] .node-in {
  opacity: 0;
  filter: blur(8px);
  transform: translateY(14px);
  /* leaving is quicker than arriving */
  transition: opacity 0.4s ease, filter 0.4s ease, transform 0.4s ease;
}
/* the download block doesn't travel: the last arrow is aimed at the logo
   inside it, and that has to be measured where it will finally sit */
.js .cta[data-reveal] .node-in { transform: none; }
.js [data-reveal].is-in .node-in {
  opacity: 1;
  filter: blur(0);
  transform: none;
  transition: opacity 0.8s ease, filter 0.8s ease, transform 0.8s var(--ease);
}
/* While the page is loading — including a reload part-way down — everything
   goes straight to the state its scroll position calls for. */
.no-anim [data-reveal] .node-in,
.no-anim .mine { transition: none !important; }

/* ---- the download -------------------------------------------------------- */
.cta {
  position: relative;
  z-index: 1;
  padding: clamp(96px, 14vh, 140px) var(--gutter) clamp(96px, 16vh, 170px);
  text-align: center;
}
.cta .node-in { display: flex; flex-direction: column; align-items: center; }
.logo-slot {
  display: block;
  width: clamp(88px, 9vw, 112px);
  height: auto;
  aspect-ratio: 1;
}
/* the app's name under its icon, as on a Mac's desktop or Dock */
.app-name {
  margin: 14px 0 clamp(24px, 4vh, 34px);
  font-size: clamp(22px, 2vw, 28px);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
}

/* ---- the news strip ------------------------------------------------------ */
.news {
  padding: clamp(64px, 10vh, 110px) 0 clamp(48px, 8vh, 80px);
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.news-head { max-width: 760px; margin: 0 auto; padding: 0 var(--gutter); text-align: center; }
.news-head h2 {
  margin-top: 12px;
  font-size: clamp(28px, 3.4vw, 44px);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.026em;
  text-wrap: balance;
}
.news-sub {
  margin-top: 14px;
  font-size: clamp(17px, 1.5vw, 20px);
  line-height: 1.45;
  color: var(--ink-2);
  text-wrap: balance;
}
/* A real scroll container, nudged along by home.js — so it drifts by itself
   but can still be swiped, dragged or scrolled by hand. */
.ticker {
  margin-top: clamp(32px, 5vh, 52px);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* cards fade out at both edges rather than being cut off */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
  outline-offset: -3px;
}
.ticker::-webkit-scrollbar { display: none; }
.ticker-track {
  display: flex;
  gap: 16px;
  width: max-content;
  padding: 6px var(--gutter) 26px;
  list-style: none;
}
.story { flex: none; width: clamp(264px, 26vw, 330px); }
.story a {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 176px;
  padding: 20px 22px 18px;
  border-radius: 18px;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--card-shadow);
  text-decoration: none;
  transition: transform 0.45s var(--spring), border-color 0.25s ease, box-shadow 0.25s ease;
}
.story a:hover {
  transform: translateY(-3px);
  border-color: var(--glass-rim-hover);
  box-shadow: var(--glass-lift-hover);
}
.story-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  font-size: 12.5px;
  color: var(--ink-3);
}
/* outlet names are set as plain text — no mastheads or logos */
.outlet { font-weight: 600; letter-spacing: 0.01em; color: var(--ink); }
/* the publication's own headline does the talking */
.story-head {
  flex: 1;
  margin-top: 14px;
  font-size: 19px;
  font-weight: 600;
  line-height: 1.28;
  letter-spacing: -0.018em;
  color: var(--ink);
  text-wrap: pretty;
}
/* not yet fetched by the fill-news-headlines script */
.story-head[data-pending] { font-weight: 400; font-style: italic; color: var(--ink-3); }
.story-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  font-size: 12.5px;
  color: var(--ink-2);
}
.tag {
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--glass-fill);
  border: 1px solid var(--glass-rim);
  white-space: nowrap;
}
.go { white-space: nowrap; transition: color 0.2s ease; }
.story a:hover .go { color: rgb(var(--net)); }
.news-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px 22px;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}
.ticker-pause {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 13px 6px 10px;
  border-radius: 999px;
  font: inherit;
  font-size: 13px;
  color: var(--ink-2);
  background: var(--glass-fill);
  border: 1px solid var(--glass-rim);
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.ticker-pause:hover { color: var(--ink); background: var(--glass-fill-hover); border-color: var(--glass-rim-hover); }
.ticker-pause svg { width: 14px; height: 14px; }
.ticker-pause .i-play { display: none; }
.ticker-pause[aria-pressed="true"] .i-play { display: block; }
.ticker-pause[aria-pressed="true"] .i-pause { display: none; }
.fineprint { max-width: 620px; font-size: 12.5px; line-height: 1.5; color: var(--ink-3); text-align: center; }

@media (prefers-reduced-motion: reduce) {
  .demo, .headline, .statement, .roots, .wire { animation: none; }
  .note { transition: opacity 0.2s ease; }
  .note-inner, .glyph .core, .glyph .shuttle, .wire.cue .wire-head path { animation: none; }
  .glyph .shuttle { opacity: 0; }
  .js .glyph .strike { stroke-dashoffset: 0; }
  .js [data-reveal] .node-in { opacity: 1; filter: none; transform: none; }
  .js .mine { background-size: 100% 0.07em; }
  .story a { transition: border-color 0.25s ease; }
  .story a:hover { transform: none; }
  /* nothing moves by itself: the strip becomes an ordinary sideways scroller */
  .ticker { scroll-snap-type: x proximity; }
  .story { scroll-snap-align: center; }
  .ticker-pause { display: none; }
}
