/* Secular One, self-hosted rather than pulled from Google's CDN — same rule the
   rest of this page follows (see the import map note in index.html). Two files
   because the copy mixes scripts: the Hebrew subset carries the sentences, the
   Latin one carries "Hearo" and "24/7". unicode-range is what keeps the browser
   from fetching the Latin file on a page that never needs it.
   It is a single-weight display face — there is no bold. .pitch-line therefore
   sets font-weight: 400, because asking for 800 makes the browser synthesise a
   fake bold that smears the letterforms. */
@font-face {
  font-family: 'Secular One';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/landing/assets/fonts/secular-one-hebrew.woff2') format('woff2');
  unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
@font-face {
  font-family: 'Secular One';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/landing/assets/fonts/secular-one-latin.woff2') format('woff2');
}

:root {
  /* The page UNDER the pinned stage. Not the stage itself: as .stage collapses
     from 100vh down to --header-h its bottom edge travels upward, and this is
     the colour of everything it uncovers on the way. White, because that area
     is "past the intro" — the teal band left at the top is all that remains of
     it. It has to be declared here rather than inherited from whatever section
     happens to sit under the stage: the closing .ask section is dark teal, and
     before the pitch section existed the old white login page supplied this by
     accident. */
  --page-color: #ffffff;
  /* Live-sampled off video1's own background (see applyBgColor). Paints .stage,
     so the flat colour behind the chroma-keyed HEARO letters during video2 is
     exactly video1's background and the crossfade between them shows no seam.
     Also the pitch copy's ink — a dark teal on the lighter --letter-color. */
  --bg-color: #050505;
  /* highlighter ink — same live-sampled colour as the intro video's
     background (--bg-color), not a fixed hue. Ties the stroke back to the
     video above instead of an arbitrary highlighter yellow. */
  --marker-color: var(--bg-color);
  /* the CTA pill kept its own colour when the marker stopped being a fixed
     yellow, since #14100a text needs an actually-light chip under it. */
  --cta-color: #ffe24d;
  /* live-sampled off the opaque (glyph) pixels of video2 during the chroma
     key — see letterColor in script.js. This literal is only the pre-scroll
     fallback for the frames before the first keyed frame exists. */
  --letter-color: #42c7ce;
  /* Ink for the pitch copy. A FIXED dark teal rather than var(--bg-color),
     which the old single-tier copy used: --bg-color is live-sampled off video1
     every frame, so the contrast of the pitch text against --letter-color used
     to depend on whatever that sample happened to be. The three tiers now derive
     their greys from this one value with color-mix, so the hierarchy holds no
     matter what the video does.
     Kept dark, not the reference's cream: cream reads beautifully on that
     video's mid-blue but drops to ~1.6:1 on our much lighter teal. */
  --ink: #04191c;
  --header-h: 84px;
  --btn-radius: 999px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  /* .stage collapses to just the assembly's header height once scrolled past,
     exposing this background around it — so it has to be the same var .stage
     paints with, or there's a hard seam around the collapsed header. */
  background: var(--page-color);
  color: #111;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* `clip`, not `hidden`: overflow-x:hidden turns html/body into scroll
     containers, which silently breaks position:sticky against the viewport —
     .pitch-sticky just scrolled away instead of pinning. `clip` suppresses the
     same horizontal overflow without creating a scroll container. The hidden
     declaration stays first as the fallback for browsers without `clip`. */
  overflow-x: hidden;
  overflow-x: clip;
}

/* ============ PINNED STAGE ============ */

.stage {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  /* video1's own sampled background — NOT --page-color. This is the colour the
     keyed letters composite over for the whole of video2, and it has to equal
     video1's background or the crossfade at PHASE.v1End shows a colour jump.
     The white page only ever appears BELOW this box, as it shrinks. */
  background: var(--bg-color);
  z-index: 50;
  overflow: hidden;
  will-change: height;
  transition: background-color .4s ease;
}

.stage-inner {
  position: relative;
  width: 100%;
  height: 100vh;
}

.video {
  display: block;
  object-fit: cover;
  will-change: transform, opacity;
}

.video--fullscreen {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 1;
}

/* video2 + button rise together as one rigid group, at their original
   size — JS sets `top` in px each frame, no scaling involved */
.assembly {
  position: absolute;
  left: 50%;
  width: 100%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  will-change: top;
}

.video--fullwidth {
  position: relative;
  width: 100%;
  height: auto;
  /* invisible on purpose — this element is only the decode source for
     video2Canvas now; visibility (not display:none) keeps it in the flex
     flow so .video2-wrap still sizes to its real rendered height. */
  visibility: hidden;
}

.video2-wrap {
  position: relative;
  width: 100%;
}

.video2-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
}

.btn-wrap {
  position: relative;
  /* video-2-text.mp4 has ~33px of empty frame below the glyphs baked into
     the footage itself — a negative margin is needed to actually close
     that gap, positive margin-top alone can't reach past it. The extra
     -18px on top of that is the user's own further nudge. */
  margin-top: -36px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.particles {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 440px;
  height: 260px;
  max-width: 100vw;
  pointer-events: none;
  opacity: 0;
}

/* ============ SCROLL HINT ============ */

/* `fixed`, and a sibling of .stage rather than a child of it: the hint has to
   stay on screen for the entire page now, and .stage both collapses to header
   height and clips with overflow:hidden — anything inside it simply vanishes
   at that point. z-index clears .stage (50) and .pitch (10).
   Living for the whole page means it crosses backgrounds of opposite lightness:
   video1/video2's dark teal while the stage is full height, then white once the
   stage has shrunk past it, then the teal pitch section, then the white login
   page. One ink cannot serve both ends, so this is the dark-background default
   and .is-on-light below is the inverse — script.js decides which applies by
   testing whether the stage still covers the hint's box (see hintOverStage). */
.scroll-hint {
  position: fixed;
  left: 50%;
  bottom: 34px;
  /* base fallback for the single frame before script.js's first RAF tick
     takes over transform/opacity (big + centered, shrinking to this spot) */
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  pointer-events: none;
  z-index: 60;
  color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition: color .25s ease, text-shadow .25s ease;
}

/* over the white page, the teal section, or the login card */
.scroll-hint.is-on-light {
  color: var(--bg-color);
  text-shadow: none;
}

/* currentColor throughout, so the mouse and its dot inherit whatever ink
   .scroll-hint is set to and there is one place to change it */
.scroll-hint-mouse {
  width: 26px;
  height: 42px;
  border: 2px solid currentColor;
  border-radius: 14px;
  display: flex;
  justify-content: center;
  padding-top: 7px;
}

.scroll-hint-dot {
  width: 4px;
  height: 8px;
  border-radius: 2px;
  background: currentColor;
  animation: scrollHintDot 1.6s ease-in-out infinite;
}

/* (The .is-calling bob that used to live here is gone with the wall-clock
   timeline. It marked the moment the sequence finished playing itself; the pitch
   is scroll driven now, so there is no such moment — the visitor is scrolling,
   the hint is telling the truth the whole way, and the section unpins into the
   white page the instant the last sentence lands.) */

@keyframes scrollHintDot {
  0% { opacity: 1; transform: translateY(0); }
  60% { opacity: 0.3; transform: translateY(10px); }
  61% { opacity: 0; transform: translateY(10px); }
  62% { opacity: 0; transform: translateY(0); }
  100% { opacity: 1; transform: translateY(0); }
}

/* ============ LOGIN BUTTON (glassmorphism) ============ */

.login-btn {
  position: relative;
  transform: translateY(24px) scale(0.9);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 34px;
  border-radius: var(--btn-radius);
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.35);
  backdrop-filter: blur(14px) saturate(160%);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  color: #fff;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.14em;
  cursor: pointer;
  opacity: 0;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.25);
  /* clickable at any scroll position, even while still fading in/hidden at
     opacity:0 — the user wants the click to always work, not gated on the
     reveal animation finishing. */
}

.login-btn svg { flex: none; }

/* micro-nudge the label/icon within the button (user's own fine-tune) */
#loginBtn > span { transform: translate(6px, 2px); }
#loginBtn > svg { transform: translate(0, 1px); }

@keyframes btnPulse {
  0%, 100% { box-shadow: 0 8px 32px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.25), 0 0 0 0 rgba(255,255,255,0.25); }
  50% { box-shadow: 0 8px 32px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.25), 0 0 24px 6px rgba(255,255,255,0.18); }
}

.login-btn.is-pulsing {
  animation: btnPulse 2.4s ease-in-out infinite;
}

/* ============ INTRO SPACER ============ */

.intro-spacer {
  /* Long enough that a fast fling/fast-scroll can't blow through the whole
     reveal in under a second (was 650vh — easily outrun). Paired with the
     progress-easing in script.js so the sequence stays a smooth reveal at
     any scroll speed instead of a jump cut. */
  height: 1600vh;
  width: 100%;
}

/* ============ ASK THE BOT ============ */

.ask {
  position: relative;
  z-index: 10;
  /* Mirrors .pitch's own margin-top below, for the same reason in reverse:
     .pitch keeps the teal off screen until the letters have finished docking,
     and this keeps the dark closing panel off screen until the phone has
     finished flying out past the pinned letter strip. Without it the panel's
     top edge slides up behind the model while the last sentence is still
     playing. The white it exposes is the body's --page-color, exactly the run
     of white that precedes the phone sequence — so the section is bracketed by
     the same colour on both sides. */
  margin-top: 100vh;
  /* The composite the model's old frame used to sit on — rgba(0,0,0,.28) over
     --bg-color — promoted from a box to the whole panel now that the box is
     gone. color-mix rather than the resulting literal (#0b3740) so it stays
     derived from the live video sample like everything else on the page: if
     the intro's teal shifts, this shifts with it. */
  background: color-mix(in srgb, #000 28%, var(--bg-color));
  min-height: 100vh;
  /* Copy and model sit side by side rather than stacked, so the model no longer
     has to share vertical room with the text — hence much taller than the
     stacked version it replaces. Both dimensions are declared once here and read
     by .ask-stage, so the band the robot occupies can never drift from what the
     layout reserved for it. */
  --ask-model-h: min(74vh, 720px);
  --ask-model-w: min(50%, 700px);
  /* The stage stays pinned above everything and --header-h is set at runtime by
     script.js from the assembly's real height (226px in practice, not the 84px
     fallback in :root) — content starting at 0 would sit under the letters. */
  padding: calc(var(--header-h) + 5vh) clamp(1.25rem, 6vw, 5rem) 6vh;
  /* Keeps the model and the spotlight inside the panel. The model stays in its
     own section rather than reaching up over the pinned letters: letting it
     cross meant dropping this clip, dropping the z-index above, and making the
     canvas click-through — three changes that between them broke the pose and
     the LOG IN button. Not worth it. */
  overflow: hidden;
  /* Two equal columns under direction:rtl, so column 1 is the RIGHT one — copy
     on the right, model on the left. The model is absolutely positioned and so
     takes no grid slot; the left column is the empty space it fills. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  direction: rtl;
}

.ask-copy {
  position: relative;
  /* above both the model and the spotlight — the copy is the one thing here
     that must stay readable whatever the 3D scene is doing behind it */
  z-index: 2;
  grid-column: 1;
  /* max-width turns the default justify-self:stretch into start, which under
     direction:rtl is the right edge — the copy hugs the side of the screen it
     reads from, no margin trickery needed */
  max-width: 34rem;
  text-align: right;
}

.ask-head {
  margin: 0;
  font-family: 'Secular One', "Segoe UI", system-ui, sans-serif;
  font-size: clamp(1.9rem, 3.6vw, 3.3rem);
  font-weight: 400;   /* Secular One ships one weight — see the @font-face note */
  line-height: 1.12;
  letter-spacing: -0.02em;
  color: #fff;
}

/* The payoff word carries the accent so the sentence has somewhere to land. */
.ask-head .ask-accent { color: var(--letter-color); }

.ask-sub {
  margin: 1.1em 0 0;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: clamp(0.92rem, 1.15vw, 1.08rem);
  line-height: 1.6;
  max-width: 34ch;
  color: rgba(255, 255, 255, 0.66);
}

/* ---- press to talk ---- */

.ask-mic {
  position: relative;
  display: grid;
  place-items: center;
  width: 5.2rem;
  height: 5.2rem;
  margin: 2.2rem 0 0;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--letter-color);
  color: #04191c;
  cursor: pointer;
  transition: transform .18s ease, background-color .18s ease;
}

.ask-mic:hover { transform: scale(1.05); }
.ask-mic:active { transform: scale(0.97); }

.ask-mic:focus-visible {
  outline: none;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--letter-color) 35%, transparent);
}

/* The ring is a separate element rather than a box-shadow on the button so the
   pulse can scale past the button's own edge without dragging the icon with it. */
.ask-mic-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--letter-color);
  opacity: 0;
  pointer-events: none;
}

.ask-mic.is-listening {
  background: #ff5a5f;
  color: #fff;
}

.ask-mic.is-listening .ask-mic-ring {
  animation: ask-pulse 1.6s ease-out infinite;
  border-color: #ff5a5f;
}

.ask-mic.is-busy { opacity: 0.6; cursor: default; transform: none; }

@keyframes ask-pulse {
  0%   { opacity: 0.85; transform: scale(1); }
  100% { opacity: 0;    transform: scale(1.9); }
}

.ask-hint {
  margin: 0.9em 0 0;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: clamp(0.78rem, 0.95vw, 0.88rem);
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.5);
}

.ask-answer {
  margin-top: 1.4em;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: clamp(0.86rem, 1vw, 0.98rem);
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.82);
}

/* Empty by default — collapsed rather than display:none so aria-live keeps
   watching the node and announces the first answer that lands in it. */
.ask-answer:empty { margin-top: 0; }

.ask-answer .ask-bubble {
  display: inline-block;
  max-width: 34rem;
  padding: 0.85em 1.15em;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.13);
}

.ask-answer .ask-bubble.is-pending { color: rgba(255, 255, 255, 0.55); }

/* ---- the model ---- */

.ask-stage {
  position: absolute;
  bottom: 0;
  /* left: 0 is the padding box's edge, not the content box's — an absolutely
     positioned child ignores its container's padding — so the model bleeds all
     the way to the side of the screen instead of stopping at the text gutter. */
  left: 0;
  width: var(--ask-model-w);
  height: var(--ask-model-h);
  z-index: 1;
  pointer-events: none;
}

/* The canvas keeps its own pointer events — the scene's cursor tracking needs
   them — while the wrapper above stays transparent to clicks so the model can
   overlap the copy without stealing them. */
.ask-canvas {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: auto;
}

/* Set by bot.js when the runtime or the scene fails to load. The copy and the
   mic stay fully usable — the section must never become a blank hole because a
   3D model did not arrive. */
.ask-stage.is-failed { display: none; }

/* ---- cursor spotlight ---- */

/* Vanilla port of the reference's framer-motion Spotlight: same radial glow
   trailing the cursor, driven by a lerp on rAF in bot.js (this page has no
   React, and the whole rest of it already animates this way). It sits behind
   both the copy and the model, so it lights the panel rather than washing out
   the headline. */
.ask-spot {
  position: absolute;
  top: 0;
  left: 0;
  width: 420px;
  height: 420px;
  margin: -210px 0 0 -210px;   /* centres the glow on the reported point */
  z-index: 0;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(circle at center,
              color-mix(in srgb, var(--letter-color) 45%, transparent),
              transparent 70%);
  filter: blur(40px);
  opacity: 0;
  transition: opacity .25s ease;
  will-change: transform;
}

.ask.is-hot .ask-spot { opacity: 1; }

@media (max-width: 900px) {
  /* No room for two columns: back to the stacked, centred arrangement, with the
     model as a band across the bottom and the copy reserving room for it. */
  .ask {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    --ask-model-h: min(34vh, 300px);
    --ask-model-w: 100%;
    padding-top: calc(var(--header-h) + 3vh);
    padding-bottom: var(--ask-model-h);
  }
  .ask-copy { max-width: none; text-align: center; }
  .ask-head { font-size: clamp(1.5rem, 6.6vw, 2.2rem); }
  .ask-sub { max-width: none; margin-inline: auto; }
  .ask-mic { width: 4.4rem; height: 4.4rem; margin: 1.8rem auto 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* The pulse is a status indicator, not decoration — it is the only thing
     telling you the mic is live — so it slows down rather than stopping. */
  .ask-mic.is-listening .ask-mic-ring { animation-duration: 3.2s; }
}


/* ============ PITCH SCROLL SECTION ============ */

/* Scroll budget for the whole phone-flight experience. script.js maps this
   section's travel to a 0..1 progress and slices it per copy line, so making
   this taller/shorter slows/speeds the entire sequence uniformly — there are
   no hardcoded pixel distances in the timing. */
.pitch {
  position: relative;
  /* Keeps the teal off screen until the intro is completely finished.
     .stage is fixed, so #introSpacer starts the document flow and this section
     used to begin at exactly the spacer's height — meaning its top edge entered
     the viewport a full 100vh of scrolling BEFORE the intro's progress hit 1,
     and teal crept up behind the letters while they were still docking.
     One viewport of margin moves its top to spacerH + 100vh, so it starts
     rising precisely as the letters land. This is not dead scroll: the section
     is travelling up through that whole viewport. */
  margin-top: 100vh;
  /* THE sequence's speed, and the only place it is set. The section is scroll
     driven again, so the pinned range (this height minus the sticky child) is the
     scroll the whole pitch costs, and the SEG_* constants in script.js divide it
     up by ratio — raising this stretches the reading beats, the phone's crossings
     and the marker strokes together, in proportion.
     700vh is about 880px of scrolling per sentence at a laptop viewport. It went
     1100 -> 200 -> 300 -> 700 across the rewrites; the small numbers belong to the
     wall-clock versions, where the sequence ran on its own and height only bought
     it time on screen. There is no dead scroll to trim now — the sequence ends
     exactly as the section unpins, and white follows immediately.
     Do not drop below ~150vh — the sticky child unpins early and the phone
     starts sliding out from under the copy mid-sequence. */
  height: 700vh;
  background: var(--letter-color);
  transition: background-color .4s ease;
  z-index: 10;
}

/* The pinned canvas. `top` clears the collapsed HEARO header so the phone
   never flies underneath it, and the height subtracts the same amount so the
   canvas is exactly the visible area. */
.pitch-sticky {
  position: sticky;
  top: var(--header-h);
  height: calc(100vh - var(--header-h));
  overflow: hidden;
  perspective: 1400px;
  font-family: 'Secular One', "Segoe UI", system-ui, sans-serif;
  /* drag-to-orbit lives on this element (see phone3d.js) rather than on the
     canvas, so it stays live over the copy lines too */
  cursor: grab;
  /* `pan-y`, never `none`: vertical panning has to keep belonging to the page,
     or a touch visitor would be trapped inside a 620vh section. phone3d.js also
     ignores touch pointers outright. */
  touch-action: pan-y;
}

.pitch-sticky.is-grabbing { cursor: grabbing; }

/* ---- WebGL phone layer ---- */

/* Fills the pinned canvas exactly; phone3d.js maps script.js's pixel offsets
   against THIS element's clientHeight, so its box must stay 1:1 with the
   sticky area or the flight path drifts out of the frame. */
.phone-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  z-index: 1;
}

/* Set by phone3d.js only once the model is actually on screen. `visibility`
   rather than `display` on purpose: script.js still reads .phone's
   offsetHeight every frame to size the 3D model, and a display:none element
   measures 0. This keeps the CSS phone measurable but unpainted. */
.pitch-sticky.has-3d .phone-fly {
  visibility: hidden;
}

/* ---- phone flight rig (CSS fallback: no WebGL / model failed to load) ---- */

.phone-fly {
  position: absolute;
  left: 50%;
  top: 50%;
  /* base pose for the one frame before script.js's first tick; every later
     frame is overwritten wholesale (translate3d + rotateX/Y/Z) by JS */
  transform: translate3d(-50%, -50%, 0);
  transform-style: preserve-3d;
  will-change: transform, opacity;
  opacity: 0;
  z-index: 2; /* above .phone-canvas */
}

/* continuous idle bob, deliberately NOT scroll-driven — it keeps the phone
   alive even when the user stops scrolling mid-section */
/* 11s, not 6.5: at the shorter period the phone read as vibrating rather than
   floating. Longer period + slightly more travel is what makes it feel like
   it's hanging in air. Keep these three numbers in sync with the 3D twin of
   this animation in phone3d.js (search "phoneBob").
   Held off until .is-settled, which script.js sets once the teal panel has
   finished rising: while the panel is still climbing the phone is attached to
   it and must not hover independently. `animation: none` rather than
   play-state:paused, so it rests at its base pose instead of freezing at
   whatever offset the keyframes happened to be at. */
.phone-bob {
  transform-style: preserve-3d;
}

.pitch-sticky.is-settled .phone-bob {
  animation: phoneBob 11s ease-in-out infinite;
}

@keyframes phoneBob {
  0%, 100% { transform: translateY(-12px) rotate(-0.9deg); }
  50%      { transform: translateY(12px) rotate(0.9deg); }
}

.phone {
  position: relative;
  /* sized off the pinned canvas's own height (viewport minus the collapsed
     header), not viewport width — a width-driven phone is taller than the
     canvas on short viewports and gets clipped top and bottom.
     This factor and the flight path's vertical drift share one budget: at 0.80
     the phone occupies 80% of the canvas, leaving only 10% of clearance above
     and below, so PITCH_DRIFT in script.js was cut to match. Raising this
     without lowering that clips the phone off both edges. */
  height: clamp(260px, calc((100vh - var(--header-h)) * 0.8), 700px);
  width: auto;
  aspect-ratio: 9 / 19.2;
  padding: 5px;
  border-radius: clamp(30px, 3.2vw, 44px);
  /* brushed-titanium frame: the off-axis gradient stops read as edge
     highlights once the rig is rotated in 3D */
  background: linear-gradient(148deg, #6a6e75 0%, #24272c 22%, #0d0f12 55%, #2f3238 78%, #7b8087 100%);
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.28),
    0 44px 90px rgba(0, 0, 0, 0.42),
    0 0 70px 8px rgba(255, 255, 255, 0.30);
}

.phone-island {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  width: 30%;
  height: 15px;
  border-radius: 999px;
  background: #06080a;
}

.phone-screen {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: clamp(26px, 2.8vw, 39px);
  overflow: hidden;
  background: radial-gradient(circle at 50% 40%, #10343d 0%, #071a20 62%, #040e12 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
}

/* ---- live "call in progress" UI (all CSS loops, never scroll-driven) ---- */

.call-rings {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
}

.call-rings span {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 150px;
  height: 150px;
  margin: -75px 0 0 -75px;
  border-radius: 50%;
  border: 1.5px solid var(--letter-color);
  opacity: 0;
  animation: callRipple 3.6s ease-out infinite;
}

.call-rings span:nth-child(2) { animation-delay: 1.2s; }
.call-rings span:nth-child(3) { animation-delay: 2.4s; }

@keyframes callRipple {
  0%   { transform: scale(0.28); opacity: 0.55; }
  70%  { opacity: 0.12; }
  100% { transform: scale(1.5); opacity: 0; }
}

.call-status {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 13px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.14);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.call-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #3ddc84;
  animation: callDot 1.8s ease-in-out infinite;
}

@keyframes callDot {
  0%, 100% { box-shadow: 0 0 0 0 rgba(61, 220, 132, 0.65); opacity: 1; }
  50%      { box-shadow: 0 0 0 7px rgba(61, 220, 132, 0); opacity: 0.55; }
}

.call-label {
  /* forced LTR so the bidi algorithm doesn't flip it to "פעיל HEARO" */
  direction: ltr;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: #eafcff;
  white-space: nowrap;
}

.call-wave {
  position: relative;
  display: flex;
  align-items: center;
  gap: 4px;
  height: 34px;
}

.call-wave i {
  display: block;
  width: 3px;
  height: 10px;
  border-radius: 2px;
  background: var(--letter-color);
  animation: callWave 1.15s ease-in-out infinite;
}

.call-wave i:nth-child(1) { animation-delay: 0s; }
.call-wave i:nth-child(2) { animation-delay: .09s; }
.call-wave i:nth-child(3) { animation-delay: .18s; }
.call-wave i:nth-child(4) { animation-delay: .27s; }
.call-wave i:nth-child(5) { animation-delay: .36s; }
.call-wave i:nth-child(6) { animation-delay: .45s; }
.call-wave i:nth-child(7) { animation-delay: .54s; }

@keyframes callWave {
  0%, 100% { height: 8px; opacity: 0.55; }
  50%      { height: 30px; opacity: 1; }
}

/* ---- copy lines ---- */

.pitch-line {
  position: absolute;
  top: 50%;
  /* narrower than before (was 42%/560px) because the phone now takes 80% of the
     canvas height and reads much wider — the copy has to stay clear of it */
  width: min(34%, 470px);
  margin: 0;
  /* base pose only — JS rewrites transform/opacity/filter every frame */
  transform: translate3d(0, -50%, 0);
  direction: rtl;
  text-align: right;
  color: var(--ink);
  opacity: 0;
  /* `filter` is in here because the travel blur animates it every frame; without
     it the browser re-rasterises the whole text block from scratch each time. */
  will-change: transform, opacity, filter;
  z-index: 2; /* above .phone-canvas */
}

/* ---- panel hierarchy ----
   Three deliberately unequal voices, which is the whole trick of the reference:
   a tiny tracked number, one heavy display line, one quiet supporting line.
   Only the headline uses Secular One — the other two use a system sans, because
   Secular One ships a single weight and cannot supply the contrast itself. */

.pitch-kicker {
  display: block;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: clamp(0.62rem, 0.85vw, 0.76rem);
  font-weight: 700;
  letter-spacing: 0.24em;
  /* the digits are Latin; without this the bidi algorithm flips "01 — הבעיה"
     into "הבעיה — 01" and the numbering stops reading as a sequence */
  direction: ltr;
  text-align: inherit;
  color: color-mix(in srgb, var(--ink) 58%, transparent);
  margin-bottom: 0.85em;
  /* hairline rule under the kicker, as in the reference. Inline-block so it
     hugs the label instead of spanning the panel. */
  padding-bottom: 0.7em;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
  width: max-content;
  max-width: 100%;
  /* PHYSICAL, not margin-inline-*. Logical margins resolve against the ELEMENT's
     own direction, and this one is forced to ltr just above for the digits — so
     margin-inline-start here would mean the left edge, while on .pitch-sub (rtl)
     the same declaration means the right. Two different results from one line is
     exactly the trap this panel fell into. The panel is RTL by design and the
     copy hugs the right edge; say that outright. */
  margin-left: auto;
  margin-right: 0;
}

.pitch-head {
  margin: 0;
  /* above .pitch-mark, which is z-index 0 and spans the whole panel. Without this
     the positioned marker layer would paint over in-flow text. */
  position: relative;
  z-index: 1;
  font-family: 'Secular One', "Segoe UI", system-ui, sans-serif;
  /* Much larger than the old single-tier copy (2.5rem cap): the reference's
     headline is the loudest thing on screen after the model, and at the old size
     the three tiers read as one grey block. */
  font-size: clamp(2rem, 4.4vw, 4rem);
  font-weight: 400;   /* Secular One ships one weight — see the @font-face note */
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: var(--ink);
}

.pitch-sub {
  margin: 0.9em 0 0;
  /* above its own marker layer, same reason as .pitch-head */
  position: relative;
  z-index: 1;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: clamp(0.92rem, 1.2vw, 1.12rem);
  font-weight: 400;
  line-height: 1.55;
  /* 30ch keeps it to two or three lines whatever the viewport, so the panel's
     silhouette stays constant as the track scrolls it past the centre */
  max-width: 30ch;
  /* physical for the same reason as .pitch-kicker — and this is the one that was
     actually wrong: under direction:rtl, margin-inline-start is the RIGHT edge,
     so `margin-inline-start: auto` pushed this block LEFT and left the supporting
     line ending 180px short of the headline it belongs under. */
  margin-left: auto;
  margin-right: 0;
  color: color-mix(in srgb, var(--ink) 74%, transparent);
}

/* Closing CTA, mirroring the reference's final slide. Opacity and lift are
   driven by JS (see CTA_IN) so it arrives a beat after its headline settles;
   this rule only owns the resting look. */
.pitch-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  margin-top: 1.6em;
  padding: 0.85em 1.6em;
  border-radius: var(--btn-radius);
  background: var(--cta-color);
  color: #14100a;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: clamp(0.78rem, 1vw, 0.9rem);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-decoration: none;
  white-space: nowrap;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.22);
  transition: transform .22s ease, box-shadow .22s ease;
}

.pitch-cta:hover {
  transform: translateY(-2px);
  box-shadow: 0 16px 34px rgba(0, 0, 0, 0.28);
}

/* ---- highlighter ----
   One <i> per VISUAL line of the sentence, built and measured by script.js
   (buildMarks) because CSS cannot address a wrapped line. Each sits behind its
   own line of text and is wiped on by scaleX from the right — RTL, so the
   stroke travels the way a right-handed reader of Hebrew would drag a pen.
   The container is inert: all geometry is written as inline styles by JS. */
.pitch-mark {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;           /* text is z-index 1, so the ink lands behind it */
}

.pitch-mark i {
  position: absolute;
  display: block;
  transform: scaleX(0);
  /* right edge: the stroke grows leftwards, following the reading direction */
  transform-origin: 100% 50%;
  /* Marker ink rather than a flat rectangle: the horizontal stops make the
     leading edge wetter and the tail thinner, and the vertical ones fade the
     very top so it reads as a translucent nib rather than a filled box. */
  background:
    linear-gradient(90deg,
      color-mix(in srgb, var(--marker-color) 82%, transparent) 0%,
      var(--marker-color) 14%,
      var(--marker-color) 88%,
      color-mix(in srgb, var(--marker-color) 68%, transparent) 100%),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--marker-color) 55%, transparent) 0 18%,
      var(--marker-color) 34% 100%);
  /* the uneven corners are what stop it looking like a <mark> element */
  border-radius: 3px 10px 6px 12px / 8px 4px 10px 5px;
}

/* Text sits above its own marker. Both are positioned and given explicit
   z-indexes rather than relying on paint order, because .pitch-mark's children
   are absolutely positioned and would otherwise win.
   display: block (not the default inline) matters for BOTH spans below: an
   inline element that wraps across two lines is painted as two separate
   fragments, each with its OWN background-position origin — but script.js's
   background-clip:text reveal (buildMarkGroup) measures row offsets against
   ONE shared box (getBoundingClientRect() of the whole span). On a wrapped
   inline that box and the per-fragment paint origin disagree, so the second
   line's ink/reveal layers land at the wrong offset — a solid block has
   exactly one fragment, so there is only one origin and it matches. */
.pitch-line-text {
  display: block;
  position: relative;
  z-index: 1;
}

.pitch-sub-text {
  display: block;
}

/* side is assigned by script.js from the same parity the phone's flight path
   uses, so the line always lands opposite the phone */
.pitch-line.is-left  { left: 6vw; }
.pitch-line.is-right { right: 6vw; }

/* Mobile: no room for phone-beside-text, so the phone rides high and the copy
   sits full-width along the bottom. script.js narrows the flight amplitude and
   drops the -50% centering to match (see `narrow`). */
@media (max-width: 820px) {
  /* Shorter than desktop's 700vh — see the .pitch comment for what this number
     does. A phone viewport is ~2.5x taller than it is wide, so the same vh count
     is a great deal more scrolling in absolute pixels, and the copy here is
     smaller and quicker to read. */
  .pitch { height: 560vh; }

  /* copy owns the bottom of the canvas, so the phone gets a smaller slice */
  .phone { height: clamp(230px, calc((100vh - var(--header-h)) * 0.58), 480px); }

  .pitch-line {
    top: auto;
    bottom: 6vh;
    width: auto;
    transform: translate3d(0, 0, 0);
  }

  /* the phone owns the middle of a phone-sized viewport, so the panel has to
     give up a tier's worth of size to fit under it */
  .pitch-head { font-size: clamp(1.5rem, 6.4vw, 2.1rem); }
  .pitch-sub  { font-size: clamp(0.85rem, 3.4vw, 1rem); max-width: none; }
  .pitch-kicker { margin-bottom: 0.6em; padding-bottom: 0.5em; }
  .pitch-cta { margin-top: 1.1em; padding: 0.75em 1.3em; }

  .pitch-line.is-left,
  .pitch-line.is-right { left: 7vw; right: 7vw; }
}

@media (prefers-reduced-motion: reduce) {
  .phone-bob,
  .call-rings span,
  .call-dot,
  .call-wave i { animation: none; }
}

@media (max-width: 640px) {
  .login-btn { padding: 14px 28px; font-size: 13px; }
  .particles { width: 320px; height: 200px; }
}
