/* The panel system — the scroll-narrative landing design, adopted whole from
   the Bamya Call landing page and generalised so any site in sites/ can build
   a page out of it.

   Deliberately standalone: a page on this system extends panel_base.html, NOT
   base.html, and loads only this file. It shares nothing with styles.css — no
   Bootstrap, no .container, no .btn — which is what lets the two live in one
   Flask app without either one having to defend its class names. The one name
   both files use is .cta, and that is exactly why the two stylesheets are
   never on the same page.

   Motion is split two ways: ambient loops (the transcript card, the language
   marquee, the wordmark's colon) are CSS keyframes right here, and everything
   scroll- or entrance-driven is GSAP + ScrollTrigger + Lenis in panel.js,
   vendored under static/vendor/. The `js` class on <html> — stamped inline in
   panel_base.html's head — is what arms the hidden initial states. Without JS
   the class is never added and the page renders as a plain, fully visible
   document; panel.js removes it again for reduced-motion visitors.

   Section map, in source order:
     tokens · progress hairline · header + wordmark · buttons · panels ·
     shared type · layout patterns · reveals · hero · transcript card ·
     roster · flow diagram · marquee + translation demo · assist card ·
     card grid · use-case carousel · checklist · cursor spotlight ·
     scrolled header · closer + footer · system hooks · motion off */

/* The palette is the brand mark's: neon teal into violet on dark charcoal.
   Teal is you — and the AI, which works on your side of the glass; violet is
   the far end. Amber survives only as a status colour (a checklist item
   covered thinly), not as a brand colour. */
:root {
  --bg: #090b0d;
  --raise: #0f1315;
  --raise-2: #141a1c;
  --line: rgba(255, 255, 255, 0.08);
  --line-strong: rgba(255, 255, 255, 0.16);
  --text: #eef2f3;
  --dim: #98a4a8;
  /* Kept level with static/style.css by stylecheck.mjs. Lifted from
     #5e696d, which was under the 4.5:1 WCAG AA floor for text at the
     sizes this sets — the section sub-copy, the footer note, the
     transcript card's language tag. Same hue, same saturation. */
  --faint: #77858a;
  --you: #38d9be;
  --them: #a78bfa;
  --live: #45ecd0;
  --warn: #ffb454;
  --mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, Menlo, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* Lenis drives the scrolling when JS runs; native smooth scrolling would
   fight it, so it is only the no-JS fallback for the anchor links. */
html:not(.js) { scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--text);
  font: 16px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

::selection { background: rgba(56, 217, 190, 0.30); }

a { color: inherit; }

.wrap {
  width: min(1120px, calc(100% - 48px));
  margin-inline: auto;
}

/* Faint editorial side rules through the whole page on wide screens. */
@media (min-width: 1240px) {
  body::before {
    content: "";
    position: fixed;
    inset: 0;
    margin-inline: auto;
    width: 1120px;
    border-inline: 1px solid rgba(255, 255, 255, 0.04);
    pointer-events: none;
    z-index: 0;
  }
}

/* The two blooms the hero used to own, lifted off it and pinned to the
   viewport: the first section's light is the whole page's light now, sitting
   still behind everything while the panels scroll through it, rather than
   passing by once and leaving the rest on flat --bg. The percentages land
   where they always did — they used to resolve against a panel that was
   100svh and full width, which is the viewport.

   A positioned layer rather than background-attachment: fixed on the body,
   which is the shorter way to write it and is broken on iOS Safari — it sizes
   the background to the document there and does not track the viewport.

   z-index: -1, not 0: the side rules above get to be behind the content by
   tree order, being the first child, but ::after comes after the panels and at
   0 would paint over them. -1 is behind every box on the page and still above
   the canvas, which is where --bg is painted (body's background propagates
   there because html has none — give html a background and this layer is
   covered by it). */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(640px 420px at 72% 26%, rgba(56, 217, 190, 0.10), transparent 70%),
    radial-gradient(560px 420px at 62% 78%, rgba(167, 139, 250, 0.07), transparent 70%);
}

/* ─────────────────────────────────────────────── progress hairline */

/* Scrubbed by GSAP; without JS it simply stays at zero width. */
.progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg, var(--you), var(--them));
  transform-origin: 0 50%;
  transform: scaleX(0);
  z-index: 60;
}

/* ─────────────────────────────────────────────── header */

.top {
  position: fixed;
  inset: 0 0 auto 0;
  display: flex;
  align-items: center;
  gap: 28px;
  padding: 14px 28px;
  z-index: 50;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}

.top-nav {
  display: flex;
  gap: 22px;
  margin-inline: auto;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.top-nav a {
  color: var(--dim);
  text-decoration: none;
  transition: color 0.2s;
}

.top-nav a:hover { color: var(--text); }

/* EN | БГ. Plain links — each language is its own page at its own path, so the
   switcher needs no JS and survives without it. Stays visible where the
   section nav collapses: on a phone the language choice matters more than the
   shortcuts. */
.lang-switch {
  display: flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
}

.lang-switch a {
  color: var(--dim);
  text-decoration: none;
  padding: 3px 9px;
  border-radius: 999px;
  transition: color 0.2s, background 0.2s;
}

.lang-switch a:hover { color: var(--text); }

.lang-switch a[aria-current="page"] {
  color: var(--text);
  background: var(--raise);
}

@media (max-width: 720px) {
  .top-nav { display: none; }
  .top { justify-content: space-between; }
}

/* The Bamya mark, redrawn rather than imported: same boxed shape as the rest
   of the family, none of the app's CSS. */
/* Set in the system's own face at its heaviest weight — no bordered lockup, no
   inverted chip, nothing to load. Which face that is differs per platform and
   that is the point: it is the same voice as the rest of the page, since --sans
   is what every other word here is set in. The tracking is pulled in because
   at this weight the default spacing reads as three words rather than one. */
.mark {
  font-family: var(--sans);
  font-weight: 900;
  font-size: 2.03rem;
  letter-spacing: -0.045em;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
  line-height: 1;

  /* How long the arriving mark reads "bamya" before it becomes the product —
     see the arrival at the foot of this section. Four animations are keyed off
     it and they have to agree to the millisecond, since the swap is timed to
     land inside the fan's first reach; written here, once, and inherited. */
  --mark-hold: 2s;
}

/* The favicon's waveform tile, inline before the wordmark. Sized in em so the
   footer's smaller mark scales it for free. The tile is pure black with no
   border, both on request; the bars take --you and --them, which are the same
   two colours the favicon hard-codes. */
.mark-icon {
  width: 0.98em;
  height: 0.98em;
  margin-right: 0.26em;
  vertical-align: -0.14em;
}

/* The colon is a diagram, not punctuation: one point that reaches out to
   three, holds, and draws them back in. On a leads page that is the product
   in two seconds - you, and the prospects you reach - so the source dot takes
   --you and the three take --them, the same pairing the rest of the page uses
   for "your side" and "the far end".

   Drawn in SVG because it needs seven shapes (a source, three lines, three
   dots) and an element has only two pseudo-elements to give.

   The layout box is unchanged at 0.2em by 0.52em, with the SVG absolutely
   centred on it and allowed to overflow, so the wordmark's metrics do not
   move when the fan opens. An empty inline-block takes its baseline from its
   bottom margin edge, which is what puts the fan row exactly where a typeset
   colon's lower dot sits, with no nudging.

   The side margins are set by the RESTING state, not the open one. Sized for
   the fan at full spread they left the lone source dot marooned in the middle
   of a gap for most of the cycle, which is the state the mark is in whenever
   it is not animating. At 0.19em the resting dot sits a normal letter-space
   from each word and the open fan just reaches them, which reads as connected
   rather than cramped. The mark's negative tracking eats 0.045em of whatever
   is given here.

   The +3px is literal and deliberately not in em: it is a fixed gap either
   side of the fan's outer dots rather than one that scales with the mark, so
   the smaller footer wordmark gets the same visible breathing room as the
   header's instead of a proportionally tighter one. */
.mark-colon {
  display: inline-block;
  position: relative;
  vertical-align: baseline;
  width: 0.2em;
  height: 0.52em;
  margin: 0 calc(0.19em + 3px);

  /* A damped spring, sampled - the step response of a second-order system at
     a damping ratio of 0.68. It gives the dots one clear overshoot as they
     arrive and a small settle back, which a bezier cannot express at all: a
     bezier has two control points and can only ever cross its target once,
     and anything that goes past and comes back the way a thing with weight
     does has to be stated as samples. linear() is exactly that - a measured
     curve, written down. Inherited by the dots inside. */
  --mark-spring: linear(
    0, 0.084, 0.2686, 0.4793, 0.6719, 0.8254, 0.9346, 1.0033, 1.0398, 1.0533,
    1.0525, 1.044, 1.0328, 1.0218, 1.0125, 1.0056, 1.0011, 0.9985, 0.9973,
    0.9971, 1);
}

/* Square and centred on its box. In the viewBox 100 units is 1em, so the
   r13 centre dot is 0.26em across and the four corners sit 30 units out
   from it on each axis. */
.mark-colon .mc {
  position: absolute;
  left: 50%;
  top: 0;
  width: 0.62em;
  height: 0.62em;
  margin-left: -0.31em;
  overflow: visible;
}

.mc-src { fill: var(--you); }
.mc-dot { fill: var(--them); }

/* One 16s cycle, four 20%-wide turns - nw, then ne, then se, then sw - each
   travelling to the centre and back and LANDING mint, not violet. Nothing
   reverts on its own: every dot that has had its turn just sits at its
   corner, mint, while the next one goes. Only once all four have gone does
   80-84% flip all of them back to violet together, and the 84-100% hold is
   what's on screen when the loop wraps to 0%.

   That is why this is eight keyframe blocks (four dots, position and tint
   each) rather than one set of shapes reused via animation-delay: a delay
   would shift a dot's own 0%/100% rest right along with it, so it could
   never share one absolute flip-back moment with the other three. Baking
   each dot's turn into its own keyframe percentages is what lets 80% and
   84% mean the same instant for all four. */
.mc-dot {
  animation-duration: 16s;
  animation-iteration-count: infinite;
}

.mc-dot.nw { --dx: 30px;  --dy: 30px;  animation-name: mc-orbit-nw, mc-tint-nw; }
.mc-dot.ne { --dx: -30px; --dy: 30px;  animation-name: mc-orbit-ne, mc-tint-ne; }
.mc-dot.se { --dx: -30px; --dy: -30px; animation-name: mc-orbit-se, mc-tint-se; }
.mc-dot.sw { --dx: 30px;  --dy: -30px; animation-name: mc-orbit-sw, mc-tint-sw; }

@keyframes mc-orbit-nw {
  0%    { transform: translate(0, 0); }
  2%    { transform: translate(0, 0); animation-timing-function: var(--mark-spring); }
  9%    { transform: translate(var(--dx), var(--dy)); }
  13%   { transform: translate(var(--dx), var(--dy)); animation-timing-function: ease-in; }
  19%   { transform: translate(0, 0); }
  100%  { transform: translate(0, 0); }
}
@keyframes mc-orbit-ne {
  0%, 20%  { transform: translate(0, 0); }
  22%   { transform: translate(0, 0); animation-timing-function: var(--mark-spring); }
  29%   { transform: translate(var(--dx), var(--dy)); }
  33%   { transform: translate(var(--dx), var(--dy)); animation-timing-function: ease-in; }
  39%   { transform: translate(0, 0); }
  100%  { transform: translate(0, 0); }
}
@keyframes mc-orbit-se {
  0%, 40%  { transform: translate(0, 0); }
  42%   { transform: translate(0, 0); animation-timing-function: var(--mark-spring); }
  49%   { transform: translate(var(--dx), var(--dy)); }
  53%   { transform: translate(var(--dx), var(--dy)); animation-timing-function: ease-in; }
  59%   { transform: translate(0, 0); }
  100%  { transform: translate(0, 0); }
}
@keyframes mc-orbit-sw {
  0%, 60%  { transform: translate(0, 0); }
  62%   { transform: translate(0, 0); animation-timing-function: var(--mark-spring); }
  69%   { transform: translate(var(--dx), var(--dy)); }
  73%   { transform: translate(var(--dx), var(--dy)); animation-timing-function: ease-in; }
  79%   { transform: translate(0, 0); }
  100%  { transform: translate(0, 0); }
}

/* Violet from 0%, flips to mint the moment each dot lands on the centre, and
   STAYS mint - through its own trip back and everyone else's turn after it -
   until the shared 80/84% flip that every one of these four keyframe sets
   repeats verbatim, so all four move together for that one beat. */
@keyframes mc-tint-nw {
  0%    { fill: var(--them); }
  7%    { fill: var(--them); }
  9%    { fill: var(--you); }
  80%   { fill: var(--you); }
  84%   { fill: var(--them); }
  100%  { fill: var(--them); }
}
@keyframes mc-tint-ne {
  0%    { fill: var(--them); }
  27%   { fill: var(--them); }
  29%   { fill: var(--you); }
  80%   { fill: var(--you); }
  84%   { fill: var(--them); }
  100%  { fill: var(--them); }
}
@keyframes mc-tint-se {
  0%    { fill: var(--them); }
  47%   { fill: var(--them); }
  49%   { fill: var(--you); }
  80%   { fill: var(--you); }
  84%   { fill: var(--them); }
  100%  { fill: var(--them); }
}
@keyframes mc-tint-sw {
  0%    { fill: var(--them); }
  67%   { fill: var(--them); }
  69%   { fill: var(--you); }
  80%   { fill: var(--you); }
  84%   { fill: var(--them); }
  100%  { fill: var(--them); }
}

/* ---- bamya.co's glyph: circle -> triangle -> square -> circle ---- */

/* One solid shape rather than the five-dot colon, on sites whose config sets
   panel.mark_shape: "morph". The box is the same inline slot the colon uses,
   so swapping between the two changes nothing about the wordmark's layout.

   The gradient is fixed and the SHAPE is what moves - both brand colours are
   on screen the whole time, so there is no colour to keep in sync with the
   morph the way the dots' violet/mint flip has to be. */
.mark-colon-morph .mm {
  position: absolute;
  left: 50%;
  top: 50%;
  /* 0.408em, not 0.34, and the circle and square give the extra back: their
     polygons below are inset to 8.33-91.67% so they still paint at the old
     0.34em while the TRIANGLE keeps the full box and so comes out 1.2x. That
     is the only way to grow one shape on its own here - a clip-path polygon
     is a percentage of this box, so scaling the triangle's own points past
     100% would just clip it off at the edges. */
  width: 0.408em;
  height: 0.408em;
  margin: -0.204em 0 0 -0.204em;

  /* Split down the middle, mint one half and violet the other, hard stop
     rather than a blend - and only on the CIRCLE (mm-morph swaps in a flat
     fill for the other two).

     The split is what makes the circle's spin legible: it is rotationally
     symmetric, so turning a single-colour one is invisible no matter how
     fast it goes. The triangle and square give their rotation away with
     their corners and need no help.

     The background rotates with the element - transform applies to the
     painted result - so the split line turns with the shape, which is the
     whole point.

     The gradient is declared ONCE, here, against two registered colour
     properties; mm-fill animates those rather than the gradient itself (see
     the @property block above for why that is not optional).

     This is also the base state: a browser that runs no animation, and a
     reduced-motion visitor, both get the settled two-tone circle rather
     than an unclipped square. */
  --mm-a: var(--you);
  --mm-b: var(--them);
  background: linear-gradient(90deg, var(--mm-a) 0 50%, var(--mm-b) 50% 100%);
  clip-path: polygon(
    50% 8.33%, 70.83% 13.92%, 86.08% 29.17%, 91.67% 50%, 86.08% 70.83%,
    70.83% 86.08%, 50% 91.67%, 29.17% 86.08%, 13.92% 70.83%, 8.33% 50%,
    13.92% 29.17%, 29.17% 13.92%);

  animation: mm-morph 12s infinite, mm-spin 12s infinite, mm-fill 12s infinite;
}

/* Every shape is 12 points, clockwise, starting at top centre - the circle
   samples its arc every 30 degrees, the triangle lays 4 along each edge, the
   square 3 per side. Equal counts in matching order is the whole trick: CSS
   interpolates polygon() point-by-point, so mismatched counts do not morph at
   all and a mismatched ORDER turns the change into a scramble.

   Each change sits at the MIDDLE of a spin, not between two of them: the
   glyph is at rest, winds up into a turn, and swaps shape at the instant it
   is whipping round fastest, so the change is carried by the motion rather
   than performed in front of a still shape. The windows below are narrow and
   centred on 20 / 53 / 86%, which is where each leg of mm-spin peaks. */
/* Shape only. The FILL is a separate track (mm-fill) on the same 12s, and
   the split is deliberate: the shape has to flip inside the narrow window
   where the spin is fastest, while the colour wants to ease across the whole
   turn. Same duration is what keeps them honest - two tracks on the same
   clock cannot drift, only tracks on different ones can. */
@keyframes mm-morph {
  /* circle - the only two-tone shape, at rest and then winding up */
  0%, 16% {
    clip-path: polygon(
      50% 8.33%, 70.83% 13.92%, 86.08% 29.17%, 91.67% 50%, 86.08% 70.83%,
      70.83% 86.08%, 50% 91.67%, 29.17% 86.08%, 13.92% 70.83%, 8.33% 50%,
      13.92% 29.17%, 29.17% 13.92%);
  }
  /* triangle - full box, so 1.2x the other two */
  24%, 49% {
    clip-path: polygon(
      50% 0%, 60.8% 18.8%, 71.7% 37.5%, 82.5% 56.3%, 93.3% 75%, 71.7% 75%,
      50% 75%, 28.3% 75%, 6.7% 75%, 17.5% 56.3%, 28.3% 37.5%, 39.2% 18.8%);
  }
  /* square - inset a further 15% from the circle's span (to 14.58-85.42%
     rather than 8.33-91.67%), which is what keeps it the smallest of the
     three.

     The four corner points are the whole reason its 12 are not evenly spaced
     the way the circle's and triangle's are: sampling this outline at an even
     1/12 of its perimeter steps straight PAST every corner, which quietly
     renders a chamfered octagon. The corners have to be named explicitly, and
     the leftovers fill the sides. */
  57%, 82% {
    clip-path: polygon(
      50% 14.58%, 85.42% 14.58%, 85.42% 38.17%, 85.42% 61.83%, 85.42% 85.42%,
      61.83% 85.42%, 38.17% 85.42%, 14.58% 85.42%, 14.58% 61.83%, 14.58% 38.17%,
      14.58% 14.58%, 32.29% 14.58%);
  }
  /* and back to the circle, held to the end of the cycle */
  90%, 100% {
    clip-path: polygon(
      50% 8.33%, 70.83% 13.92%, 86.08% 29.17%, 91.67% 50%, 86.08% 70.83%,
      70.83% 86.08%, 50% 91.67%, 29.17% 86.08%, 13.92% 70.83%, 8.33% 50%,
      13.92% 29.17%, 29.17% 13.92%);
  }
}

/* The fill, on its own track so it can take its time. Where the shape flips
   inside a narrow window at the peak of the spin, the colour eases across
   the WHOLE leg - 10-30%, 43-63%, 76-96%, exactly the windows mm-spin turns
   in - so it is always mid-blend while the glyph is moving and settled
   whenever it is still. That is the difference between a colour that snaps
   over in a fifth of a second and one that arrives with the turn.

   It moves the two halves as separate colours rather than swapping whole
   gradients, because a gradient keyframe does not blend - it cuts. A flat
   shape is simply both halves holding the same colour, so the circle's
   split does not pop off, it slides shut as one half walks over to meet the
   other. ease-in-out on each leg keeps both ends of the blend gentle. */
@keyframes mm-fill {
  /* two-tone circle */
  0%, 10% {
    --mm-a: var(--you);
    --mm-b: var(--them);
    animation-timing-function: ease-in-out;
  }
  /* flat mint, for the triangle */
  30%, 43% {
    --mm-a: var(--you);
    --mm-b: var(--you);
    animation-timing-function: ease-in-out;
  }
  /* flat violet, for the square */
  63%, 76% {
    --mm-a: var(--them);
    --mm-b: var(--them);
    animation-timing-function: ease-in-out;
  }
  /* and the split re-opens for the circle */
  96%, 100% {
    --mm-a: var(--you);
    --mm-b: var(--them);
  }
}

/* Three separate turns, each starting and ending at a standstill - the glyph
   is NOT spinning the whole time. A leg runs 10-30%, 43-63%, 76-96%, and the
   rest of the cycle it sits still.

   Each leg is a whole 360 on a steep ease-in-out, which is what makes it
   read as "slow, then really fast, then slow again": the bezier spends most
   of the leg's angle in the middle of its time. That midpoint is exactly
   where mm-morph changes shape. A full turn also means every leg ends at the
   attitude it started from, so the shape is never left sitting askew.

   It ends on 1080deg against 0deg at 0%. That jump lands on the frame where
   the shape is a full circle AND 1080 is three whole turns, so it cannot be
   seen either way. Ending anywhere else would snap a visible corner. */
@keyframes mm-spin {
  0%      { transform: rotate(0deg); }
  10%     { transform: rotate(0deg); animation-timing-function: cubic-bezier(0.85, 0, 0.15, 1); }
  30%     { transform: rotate(360deg); }
  43%     { transform: rotate(360deg); animation-timing-function: cubic-bezier(0.85, 0, 0.15, 1); }
  63%     { transform: rotate(720deg); }
  76%     { transform: rotate(720deg); animation-timing-function: cubic-bezier(0.85, 0, 0.15, 1); }
  96%     { transform: rotate(1080deg); }
  100%    { transform: rotate(1080deg); }
}

/* The titles' split, run through the wordmark and keyed to the colon:
   nothing at all while the mark is at rest or held open, widest while a dot
   is actually travelling to or from the centre. The two words take it in
   OPPOSITE directions, so violet ends up on the outside of the mark and mint
   hugs the colon, and the whole thing breathes apart and closed with each
   trip rather than shaking.

   --abx is registered, which is what makes it interpolate — an unregistered
   custom property animates in steps, not smoothly. Where @property is missing
   the var() fallback holds the mark at zero and it simply never splits. */
@property --abx {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

/* The morph glyph's two halves, and they have to be REGISTERED to animate.
   A gradient is not smoothly interpolable in a keyframe - animating either
   `background` or `background-image` from one linear-gradient to another
   gives you a DISCRETE swap that flips at the halfway point of the segment,
   however long you make it, which reads as a hard cut no easing can soften.
   Registered <color> properties do interpolate, so the gradient is declared
   once against these two and the animation moves the colours underneath it.

   initial-value cannot reference a var(), so the palette is repeated here
   as literals; .mm immediately reassigns both from --you / --them, which is
   what actually paints, so these two only matter to the registration. Keep
   them level with the tokens at the top of the file. */
@property --mm-a {
  syntax: "<color>";
  inherits: false;
  initial-value: #38d9be;
}

@property --mm-b {
  syntax: "<color>";
  inherits: false;
  initial-value: #a78bfa;
}

/* .mark-lead / .mark-trail are the system's names for "the word before the
   colon" and "the word after it" — the original .mark-b / .mark-call are kept
   as aliases so markup copied straight off the source page still works. */
.mark-b, .mark-lead {
  text-shadow: calc(var(--abx, 0) * -1em) 0 0 var(--them),
               calc(var(--abx, 0) *  1em) 0 0 var(--you);
}

/* "amya" takes the trailing word's direction rather than one of its own, so the
   split runs on unbroken through the swap: what tears apart is what comes back. */
.mark-call, .mark-trail, .mark-amya {
  text-shadow: calc(var(--abx, 0) *  1em) 0 0 var(--them),
               calc(var(--abx, 0) * -1em) 0 0 var(--you);
}

/* Same 16s cycle as the dots, started in the same style pass, so the two stay
   in step without either one knowing about the other. Four bump-pairs, not
   one - each dot gets its own out-and-back while it takes its turn, at the
   same offsets as its mc-orbit-* keyframes - then flat across 80-100%, where
   nothing is travelling any more and all that is left is the shared colour
   flip. linear joins every stop - the shape is in the numbers, so easing
   between them would bend it twice. */
.mark { animation: mark-split 16s linear infinite; }

@keyframes mark-split {
  0%,   2%  { --abx: 0; }
  6%        { --abx: 0.112; }   /* nw out */
  9%,  13%  { --abx: 0; }
  16%       { --abx: 0.09; }    /* nw back */
  19%, 22%  { --abx: 0; }
  26%       { --abx: 0.112; }   /* ne out */
  29%, 33%  { --abx: 0; }
  36%       { --abx: 0.09; }    /* ne back */
  39%, 42%  { --abx: 0; }
  46%       { --abx: 0.112; }   /* se out */
  49%, 53%  { --abx: 0; }
  56%       { --abx: 0.09; }    /* se back */
  59%, 62%  { --abx: 0; }
  66%       { --abx: 0.112; }   /* sw out */
  69%, 73%  { --abx: 0; }
  76%       { --abx: 0.09; }    /* sw back */
  79%, 100% { --abx: 0; }
}

/* The morph glyph runs a 12s cycle, not the dots' 16s, so its wordmark needs
   its own split on the same clock - the two must be one duration or they
   drift apart permanently, since a keyframe percentage means nothing without
   the duration behind it. Three bumps, one per shape change, at the same
   16-24 / 49-57 / 82-90% windows mm-morph uses, peaking mid-change - which
   is also the fastest point of that leg's spin. */
.mark-morphing { animation: mark-split-morph 12s linear infinite; }

@keyframes mark-split-morph {
  0%,  16% { --abx: 0; }
  20%      { --abx: 0.10; }   /* circle -> triangle */
  24%, 49% { --abx: 0; }
  53%      { --abx: 0.10; }   /* triangle -> square */
  57%, 82% { --abx: 0; }
  86%      { --abx: 0.10; }   /* square -> circle */
  90%, 100%{ --abx: 0; }
}

/* ---- the arrival: bamya becomes b:leads ---- */

/* The mark arrives as the company and becomes the product: "bamya" for two
   seconds, then the tail tears apart and b:leads resolves out of it. Only where
   the macro was given `arrive` — the landing page's header, not the footer and
   not the other pages.

   The b is the anchor and does not move a pixel through any of it, which is
   what the absolute positioning here is for. "amya" is taken out of the flow
   entirely, so .mark-tail is exactly as wide as the settled colon-and-word from
   the first frame and the mark never changes width — nothing in the header
   reflows, and the b is held in place by the tile in front of it, which never
   moves either. Everything that changes is to the b's right. Animating a width
   instead would have shifted the nav beside it.

   --mark-hold delays the dots and the split together, and what that buys is
   ORDER rather than alignment. The tear lands in the flat stretch at the head
   of the cycle — mark-split holds --abx at zero until 2%, and nw does not
   start moving until then either — so the company name comes apart against a
   still mark, b:leads resolves onto the mark's resting state, and only then,
   about 150ms later, does the first corner dot reach for the centre.
   Name first, then the gesture that explains it. Aligning the tear with the
   dots' own travel was the other option and it is worse: two things moving
   through each other in the same 200ms, where the reader can follow one.

   Do not shorten the hold to close that gap. It is the cycle's own opening
   2% and belongs to the dots; making the words wait for it would have the
   mark sitting on "bamya" for longer, which is the one number here somebody
   will actually notice.

   It is a chained filter rather than a text-shadow, and here that is not a
   preference: the colon is an SVG of five circles, not glyphs, so a text-shadow
   reaches "leads" and leaves all five undistorted. Chained drop-shadows
   also compound — each shadows the result of the last, so two functions draw
   four ghosts and it reads as a streak rather than two copies. It travels LEFT,
   into the b, the one thing on the mark that is not going anywhere.

   Both ends are filter: none, so nothing survives the arrival. A filter left
   standing at zero offsets is still a filter: it holds a stacking context and a
   rasterised layer around the wordmark for the life of the page, over an SVG
   that is animating five circles on every one of those 16s cycles.

   Base state is the FINAL form — amya at zero, the form at one. A browser that
   runs no animation gets b:leads from the first frame rather than a wordmark
   stranded on the wrong word, and so does a reduced-motion visitor.

   The box that POSITIONS the outgoing word cannot be the box that fades the
   incoming one, which is why there are two. opacity applies to a whole subtree,
   not to one element's own painting, so a fading .mark-tail takes the "amya"
   nested inside it along — computed opacity 1, rendering nothing, on the one
   frame where both have to be legible at once. .mark-tail is the box and never
   fades; .mark-form is the settled wordmark and is the thing that arrives, and
   it needs no layout of its own: an inline wrapper leaves the colon and the
   word laid out exactly as they were. */
.mark-tail {
  position: relative;
  display: inline-block;
}

.mark-form {
  animation: mark-form-in 0.42s ease-out backwards;
  /* Into the back of the tear, not after it: the form is already pulling into
     focus while the word that was there is still coming apart, so the two
     overlap and it is never a cut between two still words. */
  animation-delay: calc(var(--mark-hold) + 0.16s);
}

.mark-amya {
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
  /* Toward the b: what is left of the word folds into the letter that stays. */
  transform-origin: left center;
  animation: mark-amya-out 0.21s ease-in backwards;
  animation-delay: var(--mark-hold);
}

/* The hold, and it belongs to the ARRIVING mark alone. The footer's copy and
   every other page's keep their own clock from the first frame; only the mark
   that spends two seconds saying "bamya" has a reason to make its fan wait, and
   a fan reaching out under the wrong word is the one thing that would give the
   whole trick away. */
/* Same +0.6s the dots carry (see .mark-arriving .mc-dot below) - without it
   the split runs a beat ahead of the dots forever after, not just on the
   first cycle: delay only sets where an infinite animation's FIRST
   iteration starts, so two clocks with different delays never re-sync on
   their own. */
.mark-arriving { animation-delay: calc(var(--mark-hold) + 0.6s); }

/* +0.6s on top of the hold, not just var(--mark-hold): the word's own tear
   and reveal (mark-amya-out + mark-form-in) do not finish resolving until
   0.16s + 0.42s after the hold ends, and nw's first move used to start at
   just 2% of its own cycle - too soon, so the dot was visibly reaching for
   the centre while "learn" was still pulling into focus. This buffer is
   about the gesture's start, not the name's hold, so it lives here rather
   than lengthening --mark-hold itself. */
.mark-arriving .mc-dot { animation-delay: calc(var(--mark-hold) + 0.6s); }

/* 0.21s, and it ends inside mark-split's opening flat — the tear is the only
   thing moving while it happens, which is the point of the ordering above.
   ease-in holds the word legible and then drops it, rather than fading from
   the first millisecond.

   The streak is widest at 55%, NOT at the end, and that is the whole trick of
   an exit: a filter is painted at the element's own opacity, so a smear stated
   on the last keyframe is a smear drawn at opacity zero. It has to peak while
   there is still something to smear, and what the final keyframe carries is
   only where the ghosts were heading. */
@keyframes mark-amya-out {
  0% {
    opacity: 1;
    transform: scaleX(1);
    filter: none;
  }
  55% {
    opacity: 0.5;
    transform: scaleX(0.86);
    filter: drop-shadow(-0.14em 0 0.02em var(--them))
            drop-shadow(-0.29em 0 0.06em var(--you));
  }
  100% {
    opacity: 0;
    transform: scaleX(0.7);
    filter: drop-shadow(-0.22em 0 0.05em var(--them))
            drop-shadow(-0.46em 0 0.11em var(--you));
  }
}

/* The same streak read backwards: the form arrives already torn, trailing the
   pair back toward the b, and pulls into focus. It starts at opacity 0 because
   `backwards` fill paints the first keyframe for the whole two-second hold, and
   a ghost of the fan under "bamya" is the one thing this must never show. */
@keyframes mark-form-in {
  0% {
    opacity: 0;
    filter: drop-shadow(-0.24em 0 0.05em var(--them))
            drop-shadow(-0.48em 0 0.12em var(--you));
  }
  30% {
    opacity: 0.7;
    filter: drop-shadow(-0.17em 0 0.04em var(--them))
            drop-shadow(-0.34em 0 0.08em var(--you));
  }
  65% {
    opacity: 0.94;
    filter: drop-shadow(-0.07em 0 0.02em var(--them))
            drop-shadow(-0.14em 0 0.03em var(--you));
  }
  100% {
    opacity: 1;
    filter: none;
  }
}

.mark.small { font-size: 1.58rem; }

/* ─────────────────────────────────────────────── buttons */

.cta {
  display: inline-block;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 12px 22px;
  border-radius: 999px;
  transition: transform 0.15s, background 0.2s, border-color 0.2s, color 0.2s;
}

.cta:active { transform: translateY(1px); }

.cta.solid {
  background: var(--text);
  color: var(--bg);
  border: 1px solid var(--text);
}

.cta.solid:hover { background: #fff; border-color: #fff; }

.cta.ghost {
  color: var(--text);
  border: 1px solid var(--line-strong);
}

.cta.ghost:hover { border-color: var(--text); }

.cta.lg { font-size: 15px; padding: 16px 34px; }

/* ─────────────────────────────────────────────── panels (the carousel) */

.panel {
  min-height: 100svh;
  display: grid;
  /* An explicit 100% track, not auto: an auto track is sized from content,
     so the language marquee's max-content width would widen it past the
     viewport and the centred .wrap would centre inside THAT. */
  grid-template-columns: 100%;
  align-items: center;
  position: relative;
  padding: 110px 0 70px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.panel:last-of-type { border-bottom: 0; }

/* ─────────────────────────────────────────────── shared type */

.kicker {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--dim);
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 26px;
}

.pulse {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--live);
  animation: pulse 2.2s ease-out infinite;
}

@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(69, 236, 208, 0.5); }
  70%  { box-shadow: 0 0 0 12px rgba(69, 236, 208, 0); }
  100% { box-shadow: 0 0 0 0 rgba(69, 236, 208, 0); }
}

h1 {
  font-size: clamp(2.7rem, 6.2vw, 5.2rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  font-weight: 800;
  margin-bottom: 28px;
  text-wrap: balance;
}

h1 em {
  font-style: normal;
  color: var(--live);
}

h2 {
  font-size: clamp(2rem, 4.4vw, 3.4rem);
  line-height: 1.06;
  letter-spacing: -0.03em;
  font-weight: 800;
  margin-bottom: 22px;
  text-wrap: balance;
}

h2.big { font-size: clamp(2.6rem, 7vw, 5.6rem); margin-bottom: 38px; }

/* A section title is pure white only while its panel is IN PLACE. Off the
   settle point — above or below — the brand pair separates out along the
   travel and stretches with the distance, so a title arrives into focus and
   leaves it again. landing.js writes --ab (a signed number, panels away from
   settled) and nothing else; the offsets are em, so h2.big splits as far as it
   is big, and 0 here is the whole no-JS and reduced-motion story. */
h1, h2 {
  --ab: 0;
  --ab-lead: var(--you);
  --ab-trail: var(--them);
  text-shadow:
    calc(var(--ab) * -0.34em) calc(var(--ab) * -1em) 0 var(--ab-lead),
    calc(var(--ab) *  0.34em) calc(var(--ab) *  1em) 0 var(--ab-trail);
}

/* "next." is already mint, so the mint edge would be an edge against itself.
   The pair swaps on that word alone — the split still reads as two colours
   pulling apart, and the direction is carried by the violet either way. */
.hero h1 em { --ab-lead: var(--them); --ab-trail: var(--live); }

.sec-copy > p {
  max-width: 34em;
  color: var(--dim);
  font-size: clamp(1rem, 1.35vw, 1.13rem);
}

.sec-copy > p + p { margin-top: 14px; }

.sec-copy .sub { font-size: 0.95rem; color: var(--faint); }

/* The local-only promise gets its own box rather than a subordinate clause. */
.callout {
  margin-top: 18px;
  max-width: 34em;
  border: 1px dashed rgba(56, 217, 190, 0.4);
  background: rgba(56, 217, 190, 0.06);
  border-radius: 12px;
  padding: 16px 18px;
}

.callout span {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--you);
  margin-bottom: 7px;
}

.callout p { color: var(--text); font-size: 0.98rem; line-height: 1.55; }

.callout + p { margin-top: 16px; }

.lede {
  color: var(--dim);
  font-size: clamp(1.05rem, 1.5vw, 1.25rem);
  max-width: 36em;
  margin-bottom: 34px;
}

.hl-you  { color: var(--you); }
.hl-them { color: var(--them); }

.sec-head {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 26px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.sec-head .index { color: var(--faint); }

.sec-head .tag { color: var(--dim); }

.sec-head::after {
  content: "";
  flex: 0 0 56px;
  height: 1px;
  background: var(--line-strong);
}

.actions { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }

.centered-row { justify-content: center; }

.fineprint {
  margin-top: 22px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--faint);
}

.centered { text-align: center; }

.centered .sec-head { justify-content: center; }

.centered .sec-head::after { display: none; }

.centered > p, .sec-copy.centered > p { margin-inline: auto; }

/* ─────────────────────────────────────────────── layout patterns */

.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(40px, 6vw, 90px);
  align-items: center;
}

.split.flip .sec-copy { order: 2; }
.split.flip .visual  { order: 1; }

@media (max-width: 900px) {
  .split { grid-template-columns: minmax(0, 1fr); }
  .split.flip .sec-copy { order: 1; }
  .split.flip .visual  { order: 2; }
}

/* ─────────────────────────────────────────────── reveals (GSAP-driven)

   The `js` class hides everything the script will animate in; GSAP's inline
   styles then take over. Without JS the class is absent (or removed for
   reduced motion) and everything below renders visible. */

.js .reveal,
.js .hero .kicker,
.js .hero .lede,
.js .hero .actions,
.js .hero .fineprint,
.js .hero h1,
.js .transcript,
.js .scroll-cue { opacity: 0; }

/* The headline splits into rising words: outer clips, inner translates.

   The clip is the word's own box, and at a line-height this tight that box is
   SHORTER than the glyphs in it — the half-leading goes negative, so a
   descender hangs below the box and was being cut flat ("say" lost the tail of
   its y). The padding puts that room back and the negative margin takes it out
   of the layout again: with vertical-align: bottom the margin edge is what
   aligns, so the glyphs do not move and the line box does not grow. The rise
   starts below the deeper box for the same reason — see landing.js. */
.hero h1 .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}
.hero h1 .wi { display: inline-block; }

/* Once the words have landed the clip has no further job, and the focus split
   needs room on every side — it is the widest thing the headline draws. This
   is why the hero can carry the effect at all. */
.hero h1.landed .w { overflow: visible; }


/* ─────────────────────────────────────────────── hero */

/* No background of its own — it is the fixed body::after layer now, so what
   was the hero's light stays put for the whole scroll. */

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(40px, 5vw, 80px);
  align-items: center;
}

/* minmax(0, 1fr), never a bare 1fr — a bare fr track has an automatic minimum
   of min-content, so one unbreakable child (the transcript card's nowrap
   title, a wide image) sets the floor for the whole column and pushes the
   page past the viewport. The two-column rule above already says minmax(0,
   ...) for exactly this reason; the single-column override has to say it too
   or the guard is lost at the width it matters most. */
@media (max-width: 980px) {
  .hero-grid { grid-template-columns: minmax(0, 1fr); }
}

.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 26px;
  width: 22px;
  height: 34px;
  margin-left: -11px;
  border: 1px solid var(--line-strong);
  border-radius: 12px;
}

.scroll-cue::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 7px;
  width: 3px;
  height: 7px;
  margin-left: -1.5px;
  border-radius: 2px;
  background: var(--dim);
  animation: cue 2s ease-in-out infinite;
}

@keyframes cue {
  0%, 100% { transform: translateY(0);   opacity: 1; }
  60%      { transform: translateY(9px); opacity: 0.2; }
}

/* ─────────────────────────────────────────────── the live transcript card */

.transcript {
  background: var(--raise);
  border: 1px solid var(--line);
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
}

.t-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 18px;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 12px;
  color: var(--dim);
}

.t-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #ff5b5b;
  animation: rec 1.6s ease-in-out infinite;
}

@keyframes rec { 50% { opacity: 0.25; } }

.t-live { text-transform: uppercase; letter-spacing: 0.14em; color: var(--text); }

/* min-width: 0 is what makes the ellipsis real. A flex item defaults to
   min-width: auto, which is its content width, so a nowrap title does not
   shrink — it pushes .t-head wider and takes the whole card with it, past the
   viewport on a phone. The source page never hit this because its title is
   short enough to fit; any page setting its own would. */
.t-title {
  margin-inline: auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.t-lang { color: var(--faint); }

.t-body {
  padding: 20px 18px 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 300px;
}

.t-line { display: grid; grid-template-columns: 84px 1fr; gap: 0 12px; align-items: baseline; }

/* Both the original and a translation under it live in the text column; only
   the tag sits in the first one. */
.t-line p { grid-column: 2; }
.t-line .t-tag { grid-column: 1; grid-row: 1; }

/* The column is 84px - wide enough for the longest single word among tags in
   use across the family ("TRANSLATES", "OBJECTION") to sit on one line, with
   a second word wrapping cleanly below it rather than the word itself
   splitting mid character. overflow-wrap: anywhere is the fallback for a
   future tag with a word too long even for that: it breaks mid-word instead
   of overflowing into the message column with no gap at all. */
.t-tag {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: right;
  overflow-wrap: anywhere;
}

.t-line.you  .t-tag { color: var(--you); }
.t-line.them .t-tag { color: var(--them); }
.t-line.hint .t-tag { color: var(--live); }

.t-line p { font-size: 14.5px; line-height: 1.5; }

.t-line.hint p {
  color: var(--live);
  border: 1px dashed rgba(69, 236, 208, 0.35);
  border-radius: 10px;
  padding: 8px 12px;
  font-size: 13.5px;
  background: rgba(69, 236, 208, 0.05);
}

.t-trans { color: var(--live); font-size: 13px !important; margin-top: 4px; opacity: 0.85; }

.t-typing { display: flex; gap: 5px; padding-left: 74px; }

.t-typing i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--faint);
  animation: typing 1.2s ease-in-out infinite;
}

.t-typing i:nth-child(2) { animation-delay: 0.18s; }
.t-typing i:nth-child(3) { animation-delay: 0.36s; }

@keyframes typing { 40% { transform: translateY(-4px); opacity: 1; } }

/* The conversation builds ONCE and then stays put. It used to loop on a 16s
   cycle, wiping itself at 92% and starting over, which works while every line
   is a short one - but the last line is now a full outreach draft, and no loop
   long enough to read it is short enough to be worth watching. `forwards`
   holds the last keyframe, so every line ends visible and the card settles on
   the finished conversation for as long as the visitor wants it.

   That is also why each keyframe now ENDS on the visible state rather than
   fading back out: with no cycle to reset for, the fade-out had nothing left
   to hand over to.

   8s, down from 16s: the tail the old cycle spent holding and wiping is gone,
   so the whole 8s is the conversation arriving. Each line lands about a
   quarter sooner than it used to - the last one at 7.8s rather than 10.4s. */
.l1 { animation: line-1 8s forwards; }
.l2 { animation: line-2 8s forwards; }
.l3 { animation: line-3 8s forwards; }
.l4 { animation: line-4 8s forwards; }
.l5 { animation: line-5 8s forwards; }

@keyframes line-1 {
  0%, 3%    { opacity: 0; transform: translateY(10px); }
  9%, 100%  { opacity: 1; transform: none; }
}

@keyframes line-2 {
  0%, 24%   { opacity: 0; transform: translateY(10px); }
  31%, 100% { opacity: 1; transform: none; }
}

@keyframes line-3 {
  0%, 57%   { opacity: 0; transform: translateY(10px); }
  64%, 100% { opacity: 1; transform: none; }
}

@keyframes line-4 {
  0%, 90%   { opacity: 0; transform: translateY(10px) scale(0.98); }
  97%, 100% { opacity: 1; transform: none; }
}

/* The typing dots run while lines are still arriving and stop for good just
   before the draft lands - there is nothing further to type after it. */
@keyframes line-5 {
  0%, 5%    { opacity: 0; }
  10%, 84%  { opacity: 1; }
  90%, 100% { opacity: 0; }
}

/* ─────────────────────────────────────────────── roster (botless) */

.roster {
  background: var(--raise);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.4);
}

.r-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  border-radius: 10px;
}

.r-row:not(.bot):hover { background: var(--raise-2); }

.r-avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 600;
  flex: 0 0 auto;
}

.r-avatar.a1 { background: rgba(167, 139, 250, 0.16); color: var(--them); }
.r-avatar.a2 { background: rgba(167, 139, 250, 0.16); color: var(--them); }
.r-avatar.a3 { background: rgba(56, 217, 190, 0.16); color: var(--you); }

.r-name { font-size: 15px; }

.r-glyph { margin-left: auto; width: 9px; height: 9px; border-radius: 50%; }

.r-glyph.on { background: var(--live); box-shadow: 0 0 10px rgba(69, 236, 208, 0.5); }

.r-row.bot {
  border: 1px dashed var(--line-strong);
  color: var(--faint);
  opacity: 0.75;
}

.r-row.bot .r-avatar { background: transparent; border: 1px dashed var(--line-strong); color: var(--faint); }

.r-row.bot .r-name { text-decoration: line-through; font-family: var(--mono); font-size: 13.5px; }

.r-note {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ─────────────────────────────────────────────── flow diagram (private) */

.flow { width: 100%; height: auto; }

.f-node rect {
  fill: var(--raise);
  stroke: var(--line-strong);
}

.f-node.dashed rect { stroke-dasharray: 5 5; }

.f-node text {
  fill: var(--text);
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 600;
}

.f-node text.f-dim {
  fill: var(--faint);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
}

.f-wire {
  fill: none;
  stroke-width: 1.5;
}

.f-wire.audio {
  stroke: var(--you);
  stroke-dasharray: 7 7;
  animation: wire 1.4s linear infinite;
}

.f-wire.text { stroke: var(--faint); stroke-dasharray: 2 6; }

@keyframes wire { to { stroke-dashoffset: -14; } }

.f-label {
  fill: var(--faint);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.f-label.audio-l { fill: var(--you); }

/* ─────────────────────────────────────────────── languages */

.marquee {
  margin: 46px 0 40px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

.chips-row {
  display: flex;
  gap: 10px;
  width: max-content;
  animation: slide 46s linear infinite;
}

.chips-row.rev { animation-direction: reverse; }

@keyframes slide { to { transform: translateX(-50%); } }

.chips-row span {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--dim);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 8px 16px;
  white-space: nowrap;
  flex: 0 0 auto;
}

/* The chips drive the demo card, so the rows hold still while aiming. */
.marquee:hover .chips-row { animation-play-state: paused; }

.chips-row span.is-clickable {
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s;
}

.chips-row span.is-clickable:hover { color: var(--text); border-color: var(--line-strong); }

.chips-row span.is-lang {
  color: var(--live);
  border-color: rgba(69, 236, 208, 0.5);
}

.lang-hint {
  text-align: center;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 16px;
}

.trans-demo {
  max-width: 560px;
  margin: 0 auto;
  background: var(--raise);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 26px 30px;
  text-align: left;
}

/* The green leading line IS the translation — that is the product mechanic
   the card demonstrates — so it is the line the chips change. The spoken
   original holds still underneath. */
.td-lead {
  color: var(--live);
  font-size: 1.15rem;
  font-weight: 600;
  transition: opacity 0.24s ease;
}

.trans-demo.is-swapping .td-lead { opacity: 0; }

.td-orig {
  color: var(--faint);
  font-size: 0.85rem;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

.td-lang {
  display: block;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 6px;
}

.td-text { color: inherit; }

/* ─────────────────────────────────────────────── assist */

.assist-card {
  background: var(--raise);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.4);
}

.assist-card p span {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin-bottom: 7px;
}

.a-q { color: var(--them); font-size: 1.05rem; }

.a-q span { color: rgba(167, 139, 250, 0.6); }

.a-a {
  color: var(--text);
  font-size: 0.98rem;
  border: 1px dashed rgba(69, 236, 208, 0.35);
  background: rgba(69, 236, 208, 0.05);
  border-radius: 12px;
  padding: 14px 16px;
}

.a-a span { color: var(--live); }

.a-chips { display: flex; gap: 8px; }

.a-chips span {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 6px 13px;
}

/* ─────────────────────────────────────────────── after-the-call cards */

.cards {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
  margin-top: 46px;
}

@media (max-width: 1000px) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
  .cards { grid-template-columns: minmax(0, 1fr); }
}

.card {
  background: var(--raise);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 28px 26px;
  text-align: left;
  transition: border-color 0.25s, transform 0.25s;
}

.card:hover { border-color: var(--line-strong); transform: translateY(-3px); }

.card h3 {
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  margin-bottom: 10px;
}

.card p { color: var(--dim); font-size: 0.95rem; }

/* ─────────────────────────────────────────────── use cases */

.uc { margin-top: 48px; }

.uc-tabs {
  position: relative;
  display: flex;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 36px;
}

.uc-tab {
  position: relative;
  z-index: 1;
  appearance: none;
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--dim);
  font-family: var(--mono);
  font-size: 12.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 11px 22px;
  cursor: pointer;
  transition: color 0.25s, border-color 0.25s;
}

.uc-tab:hover { color: var(--text); border-color: var(--line-strong); }

.uc-tab.is-active { color: var(--bg); border-color: transparent; }

/* The sliding pill behind the active tab, moved by landing.js. Without JS it
   stays parked under the first tab, whose .is-active colours still apply. */
.uc-ink {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background: var(--text);
  border-radius: 999px;
  transition: transform 0.45s cubic-bezier(0.22, 0.61, 0.36, 1), width 0.45s cubic-bezier(0.22, 0.61, 0.36, 1);
  z-index: 0;
}

html:not(.js) .uc-ink { display: none; }

html:not(.js) .uc-tab.is-active { background: var(--text); border-color: var(--text); }

.uc-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.uc-track::-webkit-scrollbar { display: none; }

.uc-slide {
  scroll-snap-align: start;
  padding: 6px 2px;
}

.uc-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(28px, 4.5vw, 70px);
  align-items: center;
  text-align: left;
}

@media (max-width: 900px) {
  .uc-grid { grid-template-columns: minmax(0, 1fr); }
}

.uc-copy h3 {
  font-size: 1.55rem;
  letter-spacing: -0.02em;
  margin-bottom: 12px;
}

.uc-copy p { color: var(--dim); max-width: 30em; }

.uc-knows {
  margin-top: 20px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.uc-knows span {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
  margin-right: 4px;
}

.uc-knows em {
  font-style: normal;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--dim);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 13px;
  white-space: nowrap;
}

.uc-mock { box-shadow: none; }

/* ─────────────────────────────────────────────── checklist mock */

.stack { display: flex; flex-direction: column; gap: 14px; }

.check-card {
  background: var(--raise);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 20px 22px;
}

.ck-head {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 14px;
}

.check-card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.check-card li {
  position: relative;
  padding-left: 24px;
  font-size: 14.5px;
}

.check-card li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.42em;
  width: 9px;
  height: 9px;
  border-radius: 50%;
}

.check-card li.ok::before { background: var(--live); box-shadow: 0 0 8px rgba(69, 236, 208, 0.45); }
.check-card li.thin::before { background: var(--warn); }
.check-card li.no::before { background: #ff5b5b; }

.check-card li.thin,
.check-card li.no { color: var(--dim); }

.ck-note {
  margin-top: 14px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--faint);
}

/* ─────────────────────────────────────────────── cursor spotlight */

.js .spot { position: relative; }

.js .spot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(240px 240px at var(--mx, 50%) var(--my, 50%),
              rgba(56, 217, 190, 0.08), transparent 65%);
  opacity: 0;
  transition: opacity 0.35s;
}

.js .spot:hover::after { opacity: 1; }

/* ─────────────────────────────────────────────── scrolled header */

.top { transition: padding 0.3s, background 0.3s; }

.top.scrolled {
  padding-top: 10px;
  padding-bottom: 10px;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
}

/* ─────────────────────────────────────────────── closer + footer */

.closer {
  background:
    radial-gradient(700px 460px at 50% 62%, rgba(167, 139, 250, 0.09), transparent 70%);
}

.foot { border-top: 1px solid var(--line); padding: 26px 0; }

.foot-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.foot-note { font-family: var(--mono); font-size: 12px; color: var(--faint); }

.foot-note a { color: var(--dim); text-decoration: none; }

.foot-note a:hover { color: var(--text); }

/* ─────────────────────────────────────────────── system hooks

   Everything above this line is the source page as it stands. What follows is
   what a SECOND page needs and the first one never did — kept deliberately
   short, and in the same language as the components above rather than a new
   one. */

/* The source page is seven full-height panels because it is a seven-beat
   argument. A page with a short interstitial — a logo row, a single quote —
   should not be forced to a screen of its own. */
.panel.short {
  min-height: 0;
  padding: clamp(64px, 9vw, 110px) 0;
}

.panel.flush { padding-top: 0; }

/* Narrower measure for pages that are mostly prose. */
.wrap.narrow { width: min(760px, calc(100% - 48px)); }

/* Screenshots. The source page draws every visual in CSS and SVG and so needs
   no image frame at all; a product page here is mostly screenshots, and they
   have to sit in the same raised-card language as .transcript and .roster or
   they read as pasted in. Same radius, same border, same shadow. */
.shot {
  margin: 0;
  background: var(--raise);
  border: 1px solid var(--line);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.4);
}

.shot img,
.visual img {
  display: block;
  width: 100%;
  height: auto;
}

.shot figcaption {
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}

/* The source page's footer is one row, because it has one thing to say. The
   site configs in sites/ carry footer.link_sections — titled columns — and
   flattening those into the row would drop the titles, so the system has both
   and panel_base.html picks by what the config holds. Same rules, same mono,
   same faint; only the arrangement differs. */
.foot-cols {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(auto-fit, minmax(140px, 1fr));
  gap: clamp(28px, 5vw, 64px);
  padding-bottom: 26px;
}

@media (max-width: 700px) {
  .foot-cols { grid-template-columns: 1fr 1fr; }
}

.foot-brand .foot-tagline {
  margin-top: 10px;
  max-width: 24em;
  font-size: 0.9rem;
  color: var(--faint);
}

.foot-col h4 {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 12px;
}

.foot-col ul { list-style: none; display: flex; flex-direction: column; gap: 9px; }

.foot-col a {
  font-size: 0.92rem;
  color: var(--dim);
  text-decoration: none;
  transition: color 0.2s;
}

.foot-col a:hover { color: var(--text); }

/* The rule under the columns, above the copyright line. */
.foot-bottom {
  border-top: 1px solid var(--line);
  padding-top: 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.foot-bottom p { font-family: var(--mono); font-size: 12px; color: var(--faint); }

/* A platform section's header: the product's own wordmark, at heading size,
   standing where a .sec-head would. On a portfolio page the reader's first
   question at every section is "which product is this?", and the mark answers
   it faster than a line of text can.

   The word-split is switched off here and only here. It is keyed to the
   colon's clock, so six large marks on one page would pull their letters
   apart in unison - a nice accent on one wordmark in a header bar, a
   distraction repeated down a page. The colon's own fan keeps running; those
   animations live on the .mc-* children, not on .mark. */
.plat-head { margin-bottom: 22px; }

.plat-head .mark {
  font-size: clamp(1.9rem, 3.6vw, 2.7rem);
  animation: none;
}

/* An FAQ accordion. Built on <details>/<summary> rather than buttons and
   classes, so it opens, closes and announces itself correctly with no JS at
   all — panel.js only adds the height animation and the one-open-at-a-time
   behaviour on top of what the element already does. Same hairlines as the
   footer columns, same dim body copy as the section prose. */
.faq {
  max-width: 46em;
  margin: 44px auto 0;
  border-top: 1px solid var(--line);
}

.faq details { border-bottom: 1px solid var(--line); }

.faq summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  padding: 20px 0;
  cursor: pointer;
  font-size: 1.05rem;
  font-weight: 600;
  list-style: none;
  transition: color 0.2s;
}

/* The default disclosure triangle, removed both ways — WebKit needs its own
   pseudo-element and everyone else honours list-style on the summary. */
.faq summary::-webkit-details-marker { display: none; }

.faq summary:hover { color: var(--live); }

/* The marker is drawn, not typed: a plus whose vertical bar collapses into a
   minus as the item opens. Two spans would do it too, but a pseudo-element
   pair keeps the markup to the question itself. */
.faq summary::after,
.faq summary::before {
  content: "";
  position: absolute;
  right: 0;
  width: 13px;
  height: 1.5px;
  margin-top: 0.62em;
  background: var(--dim);
  transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1), background 0.2s;
}

.faq summary { position: relative; padding-right: 34px; }

.faq summary::before { transform: rotate(90deg); }

.faq details[open] summary::before { transform: rotate(0deg); }

.faq details[open] summary { color: var(--text); }

.faq details[open] summary::after,
.faq details[open] summary::before { background: var(--live); }

/* overflow: hidden is what lets panel.js animate the height from 0. */
.faq-a { overflow: hidden; }

.faq-a > div { padding: 0 34px 22px 0; }

.faq-a p { color: var(--dim); font-size: 0.98rem; line-height: 1.7; }

.faq-a p + p { margin-top: 12px; }

.faq-a strong { color: var(--text); font-weight: 600; }

.faq-a ul { list-style: none; display: flex; flex-direction: column; gap: 8px; }

.faq-a li {
  position: relative;
  padding-left: 20px;
  color: var(--dim);
  font-size: 0.98rem;
}

.faq-a li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 0.62em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--you);
}

/* A branded CTA. The source page only ever needs white-solid and ghost, both
   of which are colourless on purpose so the two accents stay meaningful. A
   product page usually wants its primary action to carry the accent, so this
   is offered rather than assumed — .solid is still the default. */
.cta.accent {
  background: var(--you);
  color: var(--bg);
  border: 1px solid var(--you);
  font-weight: 600;
}

.cta.accent:hover { background: var(--live); border-color: var(--live); }

/* ─────────────────────────────────────────────── motion off */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .t-line, .t-typing { opacity: 1 !important; transform: none !important; }

  /* The global rule above runs every animation once at 0.01ms, so a dot
     could in principle settle mid-flight if the browser sampled the 100%
     frame at an odd moment. Force each one back to its own corner instead -
     that IS the mark's rest state, four dots and a centre, no colon at all
     otherwise. */
  .mc-dot { transform: none !important; }

  /* Same reasoning for the morph glyph: let its base state stand, which is
     the circle, rather than freezing it mid-turn on some arbitrary shape. */
  .mark-colon-morph .mm { animation: none !important; transform: none !important; }

  /* Both halves of the arrival, by name, and the reason is the same shape as
     the fan's above: the global rule crushes DURATIONS and leaves DELAYS alone,
     so what would survive here is the two-second hold followed by a cut — the
     motion removed and the surprise kept. Taking the names off lets the base
     state stand, which is the settled mark, from the first frame. */
  .mark-amya, .mark-form { animation: none; }
}
