/* Home page only.
 *
 * The hero's strategy builder, the ticker, and the animated demo panels. Everything
 * here is scoped to elements that exist on one page; anything that appeared on a second page was
 * promoted into `components.css` instead.
 */

/* -- The download anchor ----------------------------------------------------------------------- */

/* Every "Get the app" link on the site points at #download, the hero's store-badge row. Left to
   the browser, the anchor lands at the very top of the viewport — underneath the sticky nav, so
   the badges arrived hidden. 40vh parks them around the middle of the screen instead, with the
   hero copy above them for context on what the reader just asked to download. */
#download { scroll-margin-top: 40vh; }

/* -- Ticker ------------------------------------------------------------------------------------ */

@keyframes wp-ticker {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

.ticker { overflow: hidden; }
.ticker__track {
  display: flex;
  width: max-content;
  animation: wp-ticker 38s linear infinite;
}

/* **Held still while it is being read.** A strip of prices that slides away from the cursor is a
   strip nobody can look at; stopping it on hover is the difference between decoration and something
   the reader is allowed to use. The whole track pauses, not the hovered copy, because the two
   halves are one seamless loop. */
@media (hover: hover) {
  .ticker:hover .ticker__track { animation-play-state: paused; }
}

.ticker__row {
  display: flex;
  align-items: center;
  gap: 44px;
  padding: 13px 22px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  white-space: nowrap;
}

.ticker__sym {
  color: var(--text-muted);
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: color 0.18s ease, border-color 0.18s ease;
}

.ticker__sym > span { color: var(--text); font-weight: 600; }
.ticker__sym--top > span { color: var(--brand-light); font-weight: 600; }

@media (hover: hover) {
  .ticker__sym:hover { color: var(--text); border-bottom-color: var(--brand); }
}

/* -- Live-ish indicators ----------------------------------------------------------------------- */

@keyframes wp-pulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

.pulse-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--positive);
  animation: wp-pulse 2s ease-in-out infinite;
}

/* The two animations above run for as long as the page is open. `base.css` already reduces them
   to a single frame under `prefers-reduced-motion`, which is the setting they most affect. */

/* -- The hero's atmosphere ----------------------------------------------------------------------- */

/* The ground behind the hero, back to front: a wash of navy, two glows drifting on their own slow
 * clocks, a field of hairlines that twists across the section, and one pulse of light travelling
 * along a line of it. The grain the wrapper already carries sits over all of it. Before this the
 * hero was one flat colour with a single still glow, which is the thing that made the page read as
 * a template.
 *
 * **Nothing here repaints.** Everything that moves is moved by `transform` and `opacity` alone,
 * which a browser hands to the compositor; the lines are drawn once and left. That is a rule, not
 * a detail: the first version swayed the lines and ran the pulse as a dash along an SVG path, both
 * of which repaint the whole drawing every frame, and on a machine without a healthy GPU that took
 * the page from 60 frames a second to 4. For the same reason the fade at the foot of the section is
 * a gradient laid over the layers rather than a mask on them — a mask has to be re-applied to
 * whatever moves beneath it.
 *
 * All of it is CSS and one inline partial (`src/partials/hero-lines.html`, written by
 * `tools/make_hero_lines.py`) — no image, no canvas, no request.
 */

/* Two classes, to outrank `.grain > *`, which would otherwise make this a relatively positioned
   block in the flow and push the hero down by its own height. Behind the content (`z-index: 1`) and,
   being earlier in the tree than the wrapper's `::after`, under the grain as well. */
.grain > .hero-atmos {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* The stage below sizes itself in fractions of this box. */
  container-type: size;
  background: radial-gradient(
    120% 90% at 72% 30%,
    color-mix(in srgb, var(--brand-blue) 13%, transparent),
    transparent 70%
  );
}

/* Dissolved into the page before the ticker's top rule, so the section has no floor. */
.hero-atmos::after {
  content: '';
  position: absolute;
  inset: auto 0 0;
  height: 32%;
  background: linear-gradient(transparent, var(--bg));
}

/* **Drawn at a quarter of the size they appear, and scaled up.** A glow is the one thing that loses
   nothing by being enlarged, and a layer kept for animation is held in memory at the size it was
   drawn, not the size it is shown: full size these two are about nine megabytes of texture between
   them, at a quarter a sixteenth of that. `--d` is the diameter on screen; the box is a quarter of
   it, placed so that scaling about its own centre lands it where a full-size one would have sat. */
.hero-glow {
  position: absolute;
  width: calc(var(--d) / 4);
  aspect-ratio: 1;
  border-radius: 50%;
  opacity: var(--atmos-glow);
  will-change: transform;
}

/* Periwinkle, up behind the phone: the scene's key light. */
.hero-glow--a {
  --d: min(980px, 150vw);
  top: calc(-34% + var(--d) * 0.375);
  right: calc(-12% + var(--d) * 0.375);
  background: radial-gradient(
    circle,
    color-mix(in srgb, var(--brand-vivid) 30%, transparent),
    color-mix(in srgb, var(--brand) 11%, transparent) 42%,
    transparent 68%
  );
  animation: hero-drift-a 17s ease-in-out infinite alternate;
}

/* Blue, low on the other side: the fill, so the copy does not sit on dead black. */
.hero-glow--b {
  --d: min(1100px, 170vw);
  bottom: calc(-46% + var(--d) * 0.375);
  left: calc(-18% + var(--d) * 0.375);
  background: radial-gradient(
    circle,
    color-mix(in srgb, var(--brand-blue) 24%, transparent),
    color-mix(in srgb, var(--brand-blue) 8%, transparent) 40%,
    transparent 66%
  );
  animation: hero-drift-b 23s ease-in-out infinite alternate;
}

/* Offsets are in the small box's own terms: 28% of a quarter-size box is 7% of the glow. */
@keyframes hero-drift-a {
  from { transform: translate3d(0, 0, 0) scale(4); }
  to { transform: translate3d(-28%, 36%, 0) scale(4.56); }
}

@keyframes hero-drift-b {
  from { transform: translate3d(0, 0, 0) scale(4.32); }
  to { transform: translate3d(36%, -28%, 0) scale(3.76); }
}

/* The stage: a 16:9 box, centred, scaled to cover the section — what `preserveAspectRatio="slice"`
   does for the drawing, done for a box, so the drawing and the pulse that rides it share one set of
   coordinates. Where container units are not understood it is simply the section's own box; the
   drawing still covers it, by its own `slice`, and the pulse, which would then be out of register
   with it, stays hidden. */
.hero-stage {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  height: 100%;
  translate: -50% -50%;
  container-type: size;
}

@supports (width: 1cqw) {
  .hero-stage {
    width: max(100cqw, 100cqh * 16 / 9);
    height: max(100cqh, 100cqw * 9 / 16);
  }
}

.hero-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  opacity: var(--atmos-lines);
}

/* The far fan drifts on a layer of its own: the compositor moves it as a finished picture, so the
   lines are never redrawn. Quick enough to be seen moving — about forty pixels each way over
   eleven seconds — and the near fan, with the pulse on it, holds still, so what changes is where
   the two cross. */
.hero-lines--far {
  will-change: transform;
  animation: hero-lines-drift 11s ease-in-out infinite alternate;
}

@keyframes hero-lines-drift {
  from { transform: translate3d(-2.6%, 2%, 0) rotate(-1.2deg) scale(1); }
  to { transform: translate3d(2.8%, -3%, 0) rotate(1.4deg) scale(1.06); }
}

/* Quieter on a phone, where the copy is centred over the field rather than standing beside it. */
@media (max-width: 900px) {
  .hero-lines { opacity: calc(var(--atmos-lines) * 0.65); }
}

/* Hairlines at any size: the drawing is scaled to cover the section, and a stroke that scaled with
   it would be a different weight on every screen. */
.hero-lines path { stroke-width: 1; vector-effect: non-scaling-stroke; }

/* The pulse: a short streak with a lit head, carried along one line of the field. The element is
   parked with its head on the stage's top-left corner and turned about that head, so a keyframe
   need only say where the head is and which way it is facing — `@keyframes hero-pulse`, generated
   beside the drawing so the two cannot drift apart. */
.hero-pulse {
  --head: 7px;
  display: none;
  position: absolute;
  left: 0;
  top: 0;
  width: 9cqw;
  height: calc(var(--head) * 2);
  margin: calc(var(--head) * -1) 0 0 calc(var(--head) - 9cqw);
  transform-origin: calc(100% - var(--head)) 50%;
  background:
    radial-gradient(
      circle at calc(100% - var(--head)) 50%,
      var(--brand-lighter) 0 1.5px,
      color-mix(in srgb, var(--brand-light) 45%, transparent) 2.5px,
      transparent var(--head)
    ),
    linear-gradient(90deg, transparent, var(--brand-lighter)) 0 50% / calc(100% - var(--head)) 2px no-repeat;
  opacity: 0;
  will-change: transform, opacity;
  animation: hero-pulse 8s linear infinite;
}

@supports (width: 1cqw) {
  .hero-pulse { display: block; }
}

/* Held still while the hero is off screen — `motion.js` adds `is-live` as it comes into view. Only
   where that script is running: without it the layers simply run. */
.js .hero-atmos:not(.is-live) * { animation-play-state: paused; }
.js .sheen:not(.is-live) { animation-play-state: paused; }

/* **Shed on a device that cannot keep up.** Compositor-only animation is free where there is a GPU
   and is not where there is none: with compositing done in software the hero's three full-section
   layers are re-blended on the CPU every frame, and measured that way the page ran at 30 frames a
   second with all of this moving and at 60 with the drift held. So `motion.js` times the frames
   the hero is actually getting and, if they are short of the screen's rate, sets one of two
   classes on `<html>`:

   - `motion-lite` holds the glows and the far fan where they are — they are the cost, and held
     they are the same picture — and drops the pulse too: alone it still cost the odd long frame
     in software, and a stutter there is what the reader notices.
   - `motion-still` is the same scene; it remains as the governor's last step.

   Paused rather than removed, so nothing jumps at the moment it happens. */
.motion-lite .hero-glow,
.motion-lite .hero-lines--far,
.motion-still .hero-glow,
.motion-still .hero-lines--far { animation-play-state: paused; }
.motion-lite .hero-pulse,
.motion-still .hero-pulse { display: none; }

/* -- The hero headline ---------------------------------------------------------------------------- */

/* Three short lines, one beat each, set large and tight. The size is fluid between the breakpoints
   so the longest line — "Always watching." — always fits its column without breaking: a display
   headline that wraps mid-phrase reads as a mistake at exactly the size meant to look deliberate. */
.hero-h1 {
  margin: 0;
  font-size: clamp(38px, 10.6vw, 60px);
  font-weight: 600;
  line-height: 1.04;
  letter-spacing: -0.022em;
}

@media (min-width: 901px) {
  .hero-h1 { font-size: clamp(46px, 5.1vw, 70px); }
}

/* -- Section rhythm ------------------------------------------------------------------------------ */

/* Every section's spacing comes from two tokens, `--sec-y` and `--stack-gap`. It used to be written
   on each section as an inline pixel value — 96, 110, 120, 130, 150 — which no stylesheet could
   shrink for a narrow screen, so a phone scrolled through the desktop's gaps. Inline gutters went
   the same way: `base.css` already gives a section the right one for each width. */
.sec {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-block: var(--sec-y);
}

.sec--narrow { max-width: 840px; }

/* A heading with its section's items following as sections of their own: the heading keeps the top
   padding, each item is held off the one before it by the stack gap, and the last closes the run. */
.sec--head { padding-bottom: 0; }
.sec--item { padding-top: var(--stack-gap); padding-bottom: 0; }
.sec--head + .sec--item { padding-top: calc(var(--stack-gap) * 0.7); }
.sec--last { padding-bottom: var(--sec-y); }

.stack { display: flex; flex-direction: column; gap: var(--stack-gap); }

/* The heading sits closer to the first row than the rows sit to each other. */
.stack__head { margin-bottom: calc(var(--stack-gap) * -0.35); }

/* -- 03 ALERT: the words, the chart, and the notification it produced ------------------------------ */

.alert-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: 'text' 'chart' 'note';
  gap: 22px;
}

.alert-row__text { grid-area: text; margin-bottom: 10px; }
.alert-row__chart { grid-area: chart; }
.alert-row__note { grid-area: note; }

@media (min-width: 901px) {
  /* The words and the notification are centred against the chart as one group: two empty rows
     take up the slack above and below them. */
  .alert-row {
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
    grid-template-areas: '. chart' 'text chart' 'note chart' '. chart';
    grid-template-rows: 1fr auto auto 1fr;
    column-gap: 64px;
    row-gap: 0;
  }
  .alert-row__text { margin-bottom: 30px; }
}

/* The chart: `src/partials/alert-chart.html`, written by `tools/make_alert_chart.py`. The SVG holds
   the shapes and scales with the column; the labels are HTML laid over it by percentage, so they
   stay the size given here instead of shrinking with the drawing. */
.ac {
  position: relative;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 1;
  color: var(--text-dim);
}

.ac svg { display: block; width: 100%; height: auto; overflow: visible; }

.ac path { fill: none; vector-effect: non-scaling-stroke; }
.ac__grid { stroke: var(--border); stroke-width: 1; }
.ac__frame { stroke: var(--border-strong); stroke-width: 1; }
.ac__fifty { stroke: var(--border-strong); stroke-width: 1; stroke-dasharray: 2 4; }
.ac path.ac__up { fill: var(--positive); stroke: var(--positive); stroke-width: 1; }
.ac path.ac__down { fill: var(--negative); stroke: var(--negative); stroke-width: 1; }
.ac__fast { stroke: var(--brand-light); stroke-width: 1.5; }
.ac__slow { stroke: var(--brand-blue); stroke-width: 1.5; }
.ac__rsi { stroke: var(--brand-light); stroke-width: 1.5; }
.ac__mark { stroke: var(--brand-light); stroke-width: 1; stroke-dasharray: 3 4; opacity: 0.8; }
.ac__halo { fill: color-mix(in srgb, var(--brand) 22%, transparent); stroke: var(--brand-light); stroke-width: 1; vector-effect: non-scaling-stroke; }
.ac__dot { fill: var(--brand-lighter); }

/* -- Picking out what a condition reads ---------------------------------------------------------
   `js/alert-chart.js` sets `data-focus` on the chart for a few seconds when a condition is pressed:
   the lines that condition is about thicken, and everything else steps back. A one-off transition
   on a small drawing, so the repaint it costs is a moment's, not a loop's. */
.ac svg path,
.ac svg circle { transition: opacity 0.35s ease, stroke-width 0.35s ease; }

.ac[data-focus] svg :is(path, circle):not(.ac__grid, .ac__frame) { opacity: 0.22; }

/* `:is(path, circle)` again on each, so these weigh the same as the dimming rule above and win by
   coming after it — without it they were lighter, and the chosen lines dimmed with the rest. */
.ac[data-focus='trend'] svg :is(path, circle):is(.ac__fast, .ac__slow),
.ac[data-focus='price'] svg :is(path, circle):is(.ac__up, .ac__down, .ac__slow, .ac__halo),
.ac[data-focus='rsi'] svg :is(path, circle):is(.ac__rsi, .ac__fifty, .ac__dot) { opacity: 1; }

.ac[data-focus='trend'] svg :is(.ac__fast, .ac__slow),
.ac[data-focus='price'] svg .ac__slow,
.ac[data-focus='rsi'] svg .ac__rsi { stroke-width: 3.2; }

.ac[data-focus='rsi'] svg .ac__fifty { stroke: var(--brand-light); stroke-dasharray: none; }

/* The labels that belong to the focused lines stay; the rest fade with their lines. */
.ac > span { transition: opacity 0.35s ease; }
.ac[data-focus] > :is(.ac__legend, .ac__level, .ac__tag) { opacity: 0.3; }
.ac[data-focus='trend'] > .ac__legend,
.ac[data-focus='rsi'] > :is(.ac__level, .ac__tag) { opacity: 1; }

.ac > span { position: absolute; white-space: nowrap; }
.ac__price { right: 0; translate: 0 -50%; }
.ac__time { bottom: 0; translate: -50% 0; }
.ac__level { right: 0; translate: 0 -50%; }
.ac__tag { left: 1.5%; translate: 0 5px; letter-spacing: 0.6px; }

/* The legend and the flag share the band above the plot, which the drawing keeps empty for them. */
.ac__legend { left: 1.5%; top: 0; height: 24px; display: flex; align-items: center; gap: 6px; }
.ac__key { display: inline-block; width: 14px; height: 2px; border-radius: 1px; }
.ac__key--fast { background: var(--brand-light); }
.ac__key--slow { background: var(--brand-blue); margin-left: 8px; }

/* Above the plot, its right edge just past the marker's dashed line, which rises to meet it. To
   the left of the marker rather than centred on it, because the marker is the last candle and
   there is no room to its right. */
.ac__flag {
  top: 0;
  display: flex;
  align-items: center;
  gap: 7px;
  translate: calc(-100% + 18px) 0;
  padding: 5px 10px;
  border: 1px solid color-mix(in srgb, var(--brand) 45%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface-raised) 88%, transparent);
  font-family: var(--font-sans);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text);
}

.ac__flag b { font-weight: inherit; }

/* On a phone the flag and the legend share a band barely wider than the two of them, so the flag
   gives up its time — the same hour is on the axis below and in the last condition. */
@media (max-width: 480px) {
  .ac__flag { font-size: 10.5px; padding: 4px 8px; gap: 6px; }
  .ac__flag b { display: none; }
}

/* The strategy's conditions, in the same form the Build card writes them. */
.ac-card__head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }

/* The strategy's conditions, in the same form the Build card writes them — and buttons: pressing
   one picks out, on the chart above, the lines that condition reads. */
.ac-conds { display: flex; flex-direction: column; gap: 6px; }

.ac-cond {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-raised);
  font: inherit;
  text-align: left;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, translate 0.18s ease;
}

.ac-cond__tf {
  flex-shrink: 0;
  padding: 2px 6px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--brand) 14%, transparent);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.4px;
  color: var(--brand-light);
}

.ac-cond__text { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: 12.5px; color: var(--text); }
.ac-cond__status { flex-shrink: 0; min-width: 6.2em; text-align: right; font-size: 11px; color: var(--positive); }

/* The one that had just become true — the reason the alert fired on this candle and not before. */
.ac-cond--fresh { border-color: color-mix(in srgb, var(--brand) 55%, transparent); }
.ac-cond--fresh .ac-cond__status { color: var(--brand-light); font-weight: 600; }

/* A small chart glyph at the end of the row, that only appears when the row can be pressed —
   the affordance that says this row does something, without labelling every row "show". */
.ac-cond::after {
  content: '';
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  opacity: 0.35;
  background: var(--text-muted);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 11l4-4 3 2 5-6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 11l4-4 3 2 5-6'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: opacity 0.18s ease, background 0.18s ease;
}

@media (hover: hover) {
  .ac-cond:hover {
    border-color: color-mix(in srgb, var(--brand) 70%, transparent);
    background: color-mix(in srgb, var(--brand) 10%, var(--surface-raised));
    translate: 0 -1px;
  }
  .ac-cond:hover::after { opacity: 1; background: var(--brand-light); }
}

.ac-cond[aria-pressed='true'] {
  border-color: var(--brand-light);
  background: color-mix(in srgb, var(--brand) 16%, var(--surface-raised));
}
.ac-cond[aria-pressed='true']::after { opacity: 1; background: var(--brand-light); }

/* -- The replay --------------------------------------------------------------------------------
   `js/alert-chart.js` steps the chart in one close at a time and lets the reader drag through it.
   Everything below only applies once the script has marked the chart `is-armed`, so the markup's
   default stays the finished chart with the alert on it.

   Cheap by construction: a step is one write to the clip rectangle, a translate of the playhead and
   a few words of text — a repaint of one small drawing a few times a second, never a loop. The
   alert's own moment is one-off animations of transform and opacity. */
.ac.is-armed { touch-action: pan-y; cursor: ew-resize; }

.ac__cursor { stroke: var(--text-dim); stroke-width: 1; opacity: 0; }
.ac.is-armed:not(.is-fired) .ac__cursor { opacity: 0.55; }

/* The hour being checked, in the band above the plot where the flag will be — it becomes the flag. */
.ac > .ac__clock {
  top: 0;
  display: none;
  translate: calc(-100% + 18px) 0;
  padding: 5px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface-raised) 88%, transparent);
  font-size: 11px;
  color: var(--text-muted);
}
.ac.is-armed:not(.is-fired) > .ac__clock { display: block; }

.ac.is-armed:not(.is-fired) :is(.ac__mark, .ac__halo, .ac__dot, .ac__ripple),
.ac.is-armed:not(.is-fired) > .ac__flag { opacity: 0; }

/* On a phone the clock crosses the legend; the legend steps aside while the chart is moving. */
@media (max-width: 480px) {
  .ac.is-armed:not(.is-fired) > .ac__legend { opacity: 0; }
  .ac > .ac__clock { font-size: 10.5px; padding: 4px 8px; }
}

.ac__ripple { fill: none; stroke: var(--brand-light); stroke-width: 1.5; opacity: 0; }
.ac :is(.ac__halo, .ac__ripple) { transform-box: fill-box; transform-origin: center; }

@keyframes ac-halo {
  0% { transform: scale(0); }
  55% { transform: scale(1.7); }
  100% { transform: scale(1); }
}
@keyframes ac-ripple {
  0% { transform: scale(1); opacity: 0.9; }
  100% { transform: scale(5); opacity: 0; }
}
@keyframes ac-flag {
  from { opacity: 0; transform: translateY(6px) scale(0.92); }
  to { opacity: 1; transform: none; }
}

.ac.is-fired.is-burst .ac__halo { animation: ac-halo 0.7s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
.ac.is-fired.is-burst .ac__ripple { animation: ac-ripple 1.1s ease-out 0.1s both; }
.ac.is-fired.is-burst > .ac__flag { animation: ac-flag 0.45s cubic-bezier(0.22, 0.61, 0.36, 1) both; }
.ac.is-armed :is(.ac__mark, .ac__dot) { transition: opacity 0.4s ease; }

/* A condition that is not true at the hour shown: its status steps back. */
.ac-cond__status { transition: color 0.2s ease; }
.ac-cond.is-off .ac-cond__status { color: var(--text-dim); }
.ac-conds.is-armed .ac-cond--fresh:not(.is-hit) { border-color: var(--border-strong); }
.ac-conds.is-armed .ac-cond--fresh:not(.is-hit) .ac-cond__status { font-weight: 400; }

@keyframes ac-hit {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand-light) 55%, transparent); }
  100% { box-shadow: 0 0 0 10px transparent; }
}
.ac-cond.is-hit.is-burst { animation: ac-hit 0.9s ease-out both; }

/* The notification waits for the alert, then arrives — through the reveal's own transition. */
.js .alert-row__note.is-waiting { opacity: 0; transform: translateY(16px) scale(0.97); }

@keyframes ac-note {
  0% { box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35), 0 0 0 0 color-mix(in srgb, var(--brand-light) 60%, transparent); }
  100% { box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35), 0 0 0 14px transparent; }
}
.alert-row__note.is-pop { animation: ac-note 1.2s ease-out 0.25s both; }

/* -- Hero -------------------------------------------------------------------------------------- */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3xl);
  align-items: center;
  padding-block: 72px 0;
}

@media (max-width: 900px) {
  .hero {
    grid-template-columns: minmax(0, 1fr);
    gap: 36px;
    padding: 44px var(--gutter-sm) 0;
  }
}

/* Used by the "Conditions met" reveal. An animation rather than a transition, so it does not depend
   on a frame elapsing between two style writes. */
@keyframes wp-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The demo replay/rerun buttons are inline-styled spans, so the hover reaches for a property the
   inline style does not set — a fill — rather than fighting it over border colour. */
[data-ai-replay] > span,
[data-ev-rerun] {
  transition: background 0.15s ease;
}
[data-ai-replay] > span:hover,
[data-ev-rerun]:hover {
  background: color-mix(in srgb, var(--brand) 14%, transparent);
}


/* -- The gradient headline's sheen --------------------------------------------------------------- */

/* Two background layers on one clipped element: the brand gradient the design drew, unchanged, and
 * a narrow highlight travelling across it. Moving the highlight rather than the gradient is what
 * lets the headline sit at rest in exactly its original colours between passes — a gradient whose
 * own position animates is never quite the colour it was designed as.
 */
.sheen {
  background-image:
    linear-gradient(100deg, transparent 38%, var(--sheen) 50%, transparent 62%),
    linear-gradient(90deg, var(--brand), var(--brand-light));
  background-size: 240% 100%, 100% 100%;
  background-position: 190% 0, 0 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Twice, then it rests. A gradient clipped to text cannot be moved by the compositor — every
     frame of the pass repaints the headline — so left running it was the one thing keeping the
     main thread awake on an otherwise idle hero. Two passes is the flourish; the rest was cost. */
  animation: wp-sheen 7s ease-in-out 2;
}

@keyframes wp-sheen {
  0%, 22% { background-position: 190% 0, 0 0; }
  70%, 100% { background-position: -90% 0, 0 0; }
}

/* -- The three counters -------------------------------------------------------------------------- */

/* The block that states the product's whole claim. Each card counts its figure in as it arrives and
 * then plays a small piece of its own sentence: the missed entry is caught, the many screens
 * collapse into one, a day of market sessions lies end to end.
 *
 * **The pictures play on arrival, not on hover.** They were hover-only, which meant a phone never
 * showed them at all and a desktop showed three rows of grey marks until the pointer happened to
 * cross one — they read as unfinished rather than as waiting. `is-in` is the class the reveal adds
 * when a card scrolls into view, so each picture now draws itself once, there, and stays drawn.
 * Without the reveal script the cards are shown already drawn.
 */
.stats { margin-top: 24px; text-align: left; }

.stat {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 24px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
}

.stat__num {
  font-family: var(--font-mono);
  font-size: 24px;
  font-weight: 600;
  color: var(--brand-light);
  /* The figure is what the eye lands on, and a counter whose value narrows as it counts down would
     otherwise drag the two lines under it about. One reserved line holds them still. */
  line-height: 1.25;
  min-height: 30px;
}

.stat__label { font-size: var(--text-md); font-weight: 600; color: var(--text); }
.stat__body { font-size: var(--text-sm); line-height: 1.55; color: var(--text-muted); }

/* The illustration sits on the floor of the card whatever length the sentence above it runs to, so
   the three line up across the row. */
.stat__art {
  margin-top: auto;
  padding-top: 20px;
  height: 44px;
  box-sizing: content-box;
  position: relative;
}

/* -- 0 missed entries: the scan catches the one that mattered ------------------------------------ */

.catch {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 44px;
}

.catch__bar {
  flex: 1;
  border-radius: 2px;
  background: var(--border-strong);
  height: 100%;
  transform: scaleY(0.2);
  transform-origin: bottom;
  transition: transform 0.5s cubic-bezier(0.34, 1.3, 0.64, 1), background 0.5s ease,
              box-shadow 0.5s ease;
  transition-delay: calc(240ms + var(--i) * 34ms);
}

.stat.is-in .catch__bar,
html:not(.js) .catch__bar {
  transform: scaleY(var(--h, 0.5));
  background: color-mix(in srgb, var(--brand) 42%, transparent);
}

/* The one the strategy was waiting for. */
.stat.is-in .catch__bar--hit,
html:not(.js) .catch__bar--hit {
  transform: scaleY(1);
  background: var(--positive);
  box-shadow: 0 0 14px color-mix(in srgb, var(--positive) 45%, transparent);
}

/* -- 1 screen to check: many charts become one list ---------------------------------------------- */

.fan { position: relative; height: 44px; }

.fan__card {
  position: absolute;
  top: 0;
  left: 0;
  width: 64px;
  height: 44px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--surface-raised);
  transform: translateX(calc(var(--i) * 36px)) rotate(calc((var(--i) - 1.5) * 5deg));
  transition: transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1), border-color 0.6s ease,
              opacity 0.6s ease;
  transition-delay: 0.7s;
}

/* Three hairlines inside each one, so the shapes read as charts rather than as blank tiles. */
.fan__card::before,
.fan__card::after {
  content: '';
  position: absolute;
  left: 9px;
  right: 9px;
  height: 2px;
  border-radius: 1px;
  background: var(--border-strong);
}
.fan__card::before { top: 12px; box-shadow: 0 16px 0 var(--border-strong); }
.fan__card::after { top: 20px; right: 26px; background: var(--brand); }

/* They arrive fanned out — four screens to check — and then gather into one tidy stack with only
   the card on top still lit. */
.stat.is-in .fan__card,
html:not(.js) .fan__card {
  transform: translateX(calc(var(--i) * 7px)) rotate(0deg);
  opacity: 0.45;
}
.stat.is-in .fan__card:last-child,
html:not(.js) .fan__card:last-child {
  opacity: 1;
  border-color: color-mix(in srgb, var(--brand) 60%, transparent);
}

/* -- 24/7: a day, and the four sessions that cover it -------------------------------------------- */

/* One 24-hour day, left to right, with the four market sessions drawn as the overlapping lanes they
 * really are: Sydney into Tokyo into London into New York, each opening before the one before it
 * has closed. The picture makes the heading's point — the lanes never leave a gap, so there is no
 * hour of the day when nothing needs watching.
 *
 * **The strip starts at 22:00 UTC, not at midnight.** A calendar day cuts the Sydney session in
 * half and leaves a stub at each end, which reads as a mistake rather than as a session that runs
 * through midnight. Beginning the strip where Sydney opens makes the same 24 hours a staircase:
 * four unbroken bars handing over left to right, which is what the sessions actually do.
 *
 * Lanes are placed from two numbers in the markup, `--from` and `--to`, percentages of the strip.
 * Keeping them in the markup keeps the arithmetic beside the times it came from.
 */
.clock {
  position: relative;
  height: 44px;
  border-radius: 8px;
  background: var(--surface-raised);
  overflow: hidden;
}

/* Quarter marks, dividing the strip into six-hour blocks. */
.clock__tick {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border-strong);
}

/* Each lane grows out from where its session opens, one after the other. `scaleX` from the left
   edge rather than an animated width: a transform is the compositor's, a width is a relayout. */
.clock__lane {
  position: absolute;
  height: 6px;
  border-radius: 3px;
  left: calc(var(--from) * 1%);
  width: calc((var(--to) - var(--from)) * 1%);
  top: calc(5px + var(--lane) * 9.5px);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: calc(300ms + var(--lane) * 160ms);
}

.stat.is-in .clock__lane,
html:not(.js) .clock__lane { transform: none; }

/* One hue at four strengths rather than four colours. A ramp survives both themes — the light
   palette collapses `--brand-light` and `--brand-deep` onto the same value, so four brand tokens
   would have rendered as three visible lanes there. */
.clock__lane--syd { background: color-mix(in srgb, var(--brand) 38%, transparent); }
.clock__lane--tok { background: color-mix(in srgb, var(--brand) 56%, transparent); }
.clock__lane--lon { background: color-mix(in srgb, var(--brand) 78%, transparent); }
.clock__lane--nyc { background: var(--brand); }

/* Where in the day the scan currently is. */
.clock__now {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 42%;
  width: 2px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--brand-lighter);
  box-shadow: 0 0 10px color-mix(in srgb, var(--brand-light) 70%, transparent);
}

/* -- 04 TEST: the three figures ------------------------------------------------------------------ */

/* Lifted out of the markup's inline styles so the size can respond to the viewport, which it has to:
   at 360px — a width a great many Android phones report — the three equal columns squeezed the
   middle one to 59px, two short of what "7 yrs" needs, and it broke at the space into two lines.
   `clamp` keeps 20px everywhere there is room and eases down to 15px on the narrowest phones, and
   `nowrap` makes the value one unbreakable token whatever the size. */
.bt-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  border-top: 1px solid var(--border);
  padding-top: 16px;
}

.bt-stat { display: flex; flex-direction: column; gap: 2px; }

.bt-stat__value {
  font-family: var(--font-mono);
  font-size: clamp(15px, 4.4vw, 20px);
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

.bt-stat__label { font-size: 11.5px; color: var(--text-muted); }

/* -- 01 BUILD: the conditions write themselves ---------------------------------------------------- */

/* The only card of the six that showed a finished state while its five neighbours animated. It now
 * writes its three conditions out on arrival, which is that section's own sentence — describe your
 * setup once — performed rather than asserted. `demos.js` owns the timing.
 */
/* Hidden only where a script is running to un-hide them, and only where the motion was wanted:
   the three conditions are the card's entire content, so a blocked script — or a reader who has
   asked for no animation, and should not have to scroll to a particular card to be given its
   text — must cost the animation rather than the card. */
@media (prefers-reduced-motion: no-preference) {
  .js [data-bd-row] { opacity: 0; transform: translateY(8px); }
  .js [data-bd-status] { opacity: 0; }
}

[data-bd-row] { transition: opacity 0.4s ease, transform 0.4s ease; }
[data-bd-row].is-in { opacity: 1; transform: none; }

/* Reserved height, so a row is never shorter while it is being typed. */
[data-bd-text] { display: inline-block; min-height: 1em; }

[data-bd-status] { transition: opacity 0.35s ease; }
[data-bd-status].is-in { opacity: 1; }

/* The caret, present only while that row is being written. */
[data-bd-text].is-typing::after {
  content: '';
  display: inline-block;
  width: 7px;
  height: 0.95em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--brand-light);
  animation: wp-caret 0.9s steps(1) infinite;
}

@keyframes wp-caret {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* -- The closing panel ----------------------------------------------------------------------------- */

/* No box. It stands on the closing band, which is already what marks it as the end of the page; a
   bordered, glowing panel inside that read as a box within a box. */
.cta-panel { position: relative; padding-block: 24px; }

/* -- Hero builder ---------------------------------------------------------------------------------- */

/* The hero's picture: the web builder, used. `src/partials/hero-builder.html` (written by
   `tools/make_hero_builder.py`) draws it finished; `js/hero-builder.js` hides what has not happened
   yet and plays the build once; the rules at the end of this block keep the watching state alive.

   **One drawing, scaled to its column.** The stage is laid out at 440x700 and scaled as a whole,
   so nothing inside it reflows between a phone and a desktop. The column sets the
   width, `aspect-ratio` the matching height, and the scale comes from the container's width where
   the browser can divide one length by another, and from a ladder of fixed sizes where it cannot. */
.m-builder {
  --hb-fit: 0.72;
  position: relative;
  width: calc(440px * var(--hb-fit));
  aspect-ratio: 440 / 700;
  margin-inline: auto;
  container-type: inline-size;
}
@media (min-width: 360px) { .m-builder { --hb-fit: 0.8; } }
@media (min-width: 400px) { .m-builder { --hb-fit: 0.88; } }
@media (min-width: 480px) { .m-builder { --hb-fit: 1; } }

@supports (width: calc(1px * calc(100cqw / 1px))) {
  .m-builder { width: min(100%, 470px); }
  .m-builder > .hb { --hb-fit: calc(100cqw / 440px); }
}

.hb {
  --hb-win: color-mix(in srgb, var(--bg) 55%, var(--surface));
  position: absolute;
  left: 0;
  top: 0;
  width: 440px;
  height: 700px;
  transform: scale(var(--hb-fit));
  transform-origin: 0 0;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--text);
  text-align: left;
  user-select: none;
}

/* Hidden until the script has set the opening scene, and shown anyway after a few seconds, so a
   script that fails to arrive cannot leave the hero empty. */
.js .hb:not(.is-armed):not(.is-still) { opacity: 0; animation: hb-failsafe 0.4s 3s forwards; }
@keyframes hb-failsafe { to { opacity: 1; } }

/* What has not happened yet. Every one of these is visible in the markup's finished picture. */
.hb [data-hb-pend] { transition: opacity 0.35s ease; }
.hb.is-armed [data-hb-pend]:not(.is-shown) { opacity: 0; }

/* -- The window -- */
.hb-win {
  position: absolute;
  inset: 0;
  border: 1px solid var(--border-strong);
  border-radius: 18px;
  background: var(--hb-win);
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.45), 0 0 60px color-mix(in srgb, var(--brand) 16%, transparent);
  transform-origin: 50% 0;
  transition: transform 0.9s cubic-bezier(0.65, 0, 0.35, 1), height 0.9s cubic-bezier(0.65, 0, 0.35, 1);
}
/* Stepping back, the window also drops the empty space under its cards — a one-off change of
   height, not part of the loop. The numbers match `WATCH_*` in the generator. */
.hb.is-watching .hb-win { height: 512px; transform: translateY(36px) scale(0.66); }
.hb.is-watching .hb-add { opacity: 0; }

/* Watching, the window and its markets sit in one panel, so the hero reads as a single card. It
   arrives with the step back, once: opacity and transform. Its size comes from the generator. */
.hb-frame {
  position: absolute;
  left: 0;
  width: 440px;
  border: 1px solid var(--border-strong);
  border-radius: 24px;
  background:
    radial-gradient(120% 60% at 50% 0%, color-mix(in srgb, var(--brand) 10%, transparent), transparent 70%),
    linear-gradient(180deg, color-mix(in srgb, var(--surface) 80%, transparent), color-mix(in srgb, var(--bg) 70%, transparent));
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4), 0 0 80px color-mix(in srgb, var(--brand) 14%, transparent);
  opacity: 0;
  transform: scale(0.97);
  transition: opacity 0.6s ease 0.3s, transform 0.9s cubic-bezier(0.65, 0, 0.35, 1);
}
.hb.is-watching .hb-frame { opacity: 1; transform: none; }
/* Inside the panel the window needs less lift of its own. */
.hb.is-watching .hb-win { box-shadow: 0 14px 34px rgba(0, 0, 0, 0.35); }

.hb-bar { position: absolute; left: 0; right: 0; top: 0; height: 46px; display: flex; align-items: center; gap: 6px; padding: 0 12px 0 14px; border-bottom: 1px solid var(--border); }
.hb-dot { width: 9px; height: 9px; border-radius: 50%; opacity: 0.75; }
.hb-title { display: flex; flex-direction: column; margin-left: 14px; flex: 1; min-width: 0; line-height: 1.2; }
.hb-kicker { font-size: 9.5px; color: var(--text-dim); }
.hb-name { font-size: 13.5px; font-weight: 600; white-space: nowrap; }
.hb-name.is-placeholder { color: var(--text-muted); }
.hb-long { padding: 3px 8px; border: 1px solid color-mix(in srgb, var(--positive) 35%, transparent); border-radius: 999px; background: color-mix(in srgb, var(--positive) 12%, transparent); color: var(--positive); font-size: 10px; font-weight: 600; }
.hb-create { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 13px; border-radius: 999px; background: #1C4BBF; color: #fff; font-size: 11px; font-weight: 600; transition: opacity 0.3s ease, box-shadow 0.3s ease, background 0.3s ease; }
.hb-create::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--positive); }
.hb-create.is-idle, .hb-create.is-ready { background: var(--brand); }
.hb-create.is-idle { opacity: 0.45; }
.hb-create.is-idle::before, .hb-create.is-ready::before { display: none; }
.hb-create.is-pressed { box-shadow: 0 0 0 6px color-mix(in srgb, var(--brand-light) 30%, transparent); }

/* -- The chart -- */
.hb-panel { position: absolute; left: 14px; top: 60px; width: 412px; height: 250px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); }
.hb-legend { position: absolute; left: 12px; top: 10px; display: flex; gap: 14px; font-family: var(--font-mono); font-size: 9.5px; color: var(--text-muted); }
.hb-key { display: flex; align-items: center; gap: 6px; }
.hb-key i { width: 12px; height: 2px; border-radius: 1px; }
.hb-key--met i { width: 9px; height: 7px; border-radius: 2px; background: var(--positive); }
.hb-plot { position: absolute; left: 12px; top: 30px; width: 388px; height: 214px; overflow: hidden; }
.hb-plot svg { display: block; }
.hb-grid { fill: none; stroke: var(--border); stroke-width: 1; }
.hb-up { fill: var(--positive); stroke: var(--positive); stroke-width: 1; }
.hb-down { fill: var(--negative); stroke: var(--negative); stroke-width: 1; }
.hb-ln { fill: none; stroke-width: 1.6; stroke-linejoin: round; }
.hb-ln--fast { stroke: #5B95FA; }
.hb-ln--slow { stroke: #3468E4; }
.hb-ln--rsi { stroke: #A78BFA; }
.hb-fill { fill: color-mix(in srgb, #F59E0B 16%, transparent); }
.hb-level { fill: none; stroke: var(--text-muted); stroke-width: 1; stroke-dasharray: 2 4; }
.hb-cross { fill: #A78BFA; }
.hb-v { transition: opacity 0.12s linear; }
.hb-off { opacity: 0; }
.hb-track-bg { fill: none; stroke: var(--border); stroke-width: 4; transform: translateY(2px); }
.hb-strip { opacity: 0.85; }
.hb-strip--whole { fill: var(--positive); opacity: 1; }
.hb-col { fill: color-mix(in srgb, var(--positive) 11%, transparent); transition: opacity 0.4s ease; }

/* A strip is drawn complete and uncovered by sliding its cover off to the right: a transform. */
.hb-cover { position: absolute; left: -2px; width: 392px; background: var(--surface); transform: translateX(101%); transition: transform 1.1s linear; }
.hb.is-armed .hb-cover:not(.is-shown) { transform: none; }

/* Where every condition holds: a dot, and a ring that goes out each time the cursor passes it. */
.hb-mark { position: absolute; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; background: var(--positive); box-shadow: 0 0 10px var(--positive); }
.hb-mark::after { content: ''; position: absolute; inset: -1px; border: 1.5px solid var(--positive); border-radius: 50%; opacity: 0; }

.hb-cursor { position: absolute; left: 0; top: 4px; width: 1px; background: linear-gradient(transparent, color-mix(in srgb, var(--brand-light) 70%, transparent) 40%, color-mix(in srgb, var(--brand-light) 20%, transparent)); opacity: 0; }

/* -- Conditions -- */
.hb-label { position: absolute; left: 16px; top: 322px; font-size: 9.5px; font-weight: 600; letter-spacing: 1.2px; text-transform: uppercase; color: var(--text-dim); }
.hb-empty { position: absolute; left: 16px; right: 16px; top: 346px; font-size: 10.5px; color: var(--text-dim); opacity: 0; transition: opacity 0.3s ease; }
.hb.is-armed .hb-empty.is-shown { opacity: 1; }

.hb-card, .hb-ed { position: absolute; left: 14px; width: 412px; border-radius: 10px; }
.hb-card { height: 40px; display: flex; align-items: center; gap: 10px; padding: 0 12px; border: 1px solid var(--border-strong); background: var(--surface-raised); }
.hb.is-armed .hb-card.is-shown { animation: hb-land 0.9s ease-out; }
@keyframes hb-land { from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hb-c) 55%, transparent); } to { box-shadow: 0 0 0 10px transparent; } }
.hb-sw { width: 4px; height: 18px; border-radius: 2px; background: var(--hb-c); flex: 0 0 auto; }
.hb-card__t { flex: 1; font-family: var(--font-mono); font-size: 11.5px; }
.hb-w { padding: 2px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--text-muted); font-size: 9.5px; }

.hb-add { position: absolute; left: 14px; top: 0; width: 412px; height: 34px; display: flex; align-items: center; justify-content: center; border: 1px dashed color-mix(in srgb, var(--brand-light) 55%, transparent); border-radius: 9px; color: var(--brand-light); font-size: 11px; font-weight: 600; transition: opacity 0.25s ease; }
.hb-add.is-gone { opacity: 0; }

.hb-ed { padding: 0 14px 10px; border: 1px solid color-mix(in srgb, var(--hb-c) 60%, transparent); background: var(--surface-raised); box-shadow: 0 0 24px color-mix(in srgb, var(--hb-c) 22%, transparent); opacity: 0; transform: translateY(-6px) scale(0.98); transform-origin: 50% 0; transition: opacity 0.3s ease, transform 0.35s cubic-bezier(0.22, 0.61, 0.36, 1); z-index: 2; }
.hb-ed.is-open { opacity: 1; transform: none; }
.hb-ed__head { height: 36px; display: flex; align-items: center; gap: 10px; }
.hb-draft { font-family: var(--font-mono); font-size: 11.5px; }
.hb-draft.is-placeholder { font-family: var(--font-sans); font-weight: 600; color: var(--text-muted); }
.hb-row { height: 32px; display: flex; align-items: center; }
.hb-lab { width: 108px; flex: 0 0 auto; font-size: 10px; color: var(--text-muted); }
.hb-val { font-size: 10.5px; }
.hb-sel { position: relative; flex: 1; height: 24px; display: flex; align-items: center; padding: 0 10px; border: 1px solid var(--border-strong); border-radius: 7px; background: var(--hb-win); font-size: 10.5px; transition: border-color 0.2s ease; }
.hb-sel::after { content: ''; position: absolute; right: 10px; top: 7px; width: 5px; height: 5px; border-right: 1.5px solid var(--text-muted); border-bottom: 1.5px solid var(--text-muted); transform: rotate(45deg); }
.hb-sel.is-focus { border-color: var(--brand-light); }
.hb-sel__v.is-placeholder { color: var(--text-dim); }
.hb-list { position: absolute; left: -1px; right: -1px; top: 27px; display: flex; flex-direction: column; padding: 3px; border: 1px solid var(--border-strong); border-radius: 8px; background: var(--surface-raised); box-shadow: 0 14px 30px rgba(0, 0, 0, 0.4); opacity: 0; transform: translateY(-4px); transition: opacity 0.18s ease, transform 0.18s ease; z-index: 3; }
.hb-sel.is-open .hb-list { opacity: 1; transform: none; }
.hb-opt { height: 22px; display: flex; align-items: center; padding: 0 8px; border-radius: 5px; color: var(--text-muted); font-size: 10.5px; }
.hb-opt.is-hot { background: color-mix(in srgb, var(--brand) 32%, transparent); color: var(--text); }
.hb-prm { height: 40px; padding-top: 4px; }
.hb-prm__top { display: flex; justify-content: space-between; align-items: center; height: 18px; }
.hb-num { min-width: 34px; padding: 1px 6px; border: 1px solid var(--border-strong); border-radius: 5px; background: var(--hb-win); font-family: var(--font-mono); font-size: 10.5px; text-align: center; }
.hb-track { position: relative; height: 4px; margin-top: 8px; border-radius: 2px; background: var(--border-strong); }
.hb-track__fill { position: absolute; inset: 0; border-radius: 2px; background: var(--hb-c); transform-origin: 0 50%; }
.hb-knob { position: absolute; left: -7px; top: -5px; width: 14px; height: 14px; border: 2px solid var(--hb-c); border-radius: 50%; background: #fff; transition: box-shadow 0.2s ease; }
.hb-knob.is-held { box-shadow: 0 0 0 6px color-mix(in srgb, var(--hb-c) 28%, transparent); }
.hb-prm.is-drag .hb-track__fill, .hb-prm.is-drag .hb-knob { transition: transform var(--hb-drag, 0.95s) cubic-bezier(0.65, 0, 0.35, 1), box-shadow 0.2s ease; }
.hb-chips { display: flex; gap: 5px; }
.hb-chip { padding: 3px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--text-muted); font-family: var(--font-mono); font-size: 9.5px; transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; }
.hb-chip.is-on { border-color: var(--brand-light); background: color-mix(in srgb, var(--brand) 30%, transparent); color: var(--text); }

/* -- The pointer -- */
.hb-pointer { position: absolute; left: 0; top: 0; z-index: 5; opacity: 0; transition: opacity 0.3s ease; will-change: transform; }
.hb-pointer.is-shown { opacity: 1; }
.hb-pointer.is-moving { transition: opacity 0.3s ease, transform var(--hb-move, 0.45s) cubic-bezier(0.45, 0, 0.25, 1); }
.hb-pointer svg { display: block; fill: #fff; stroke: #09101A; stroke-width: 1.2; filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.5)); }
.hb-pointer > i { position: absolute; left: -13px; top: -13px; width: 28px; height: 28px; border: 2px solid rgba(255, 255, 255, 0.8); border-radius: 50%; opacity: 0; }
.hb-pointer.is-click > i { animation: hb-click 0.4s ease-out; }
@keyframes hb-click { from { opacity: 0.9; transform: scale(0.3); } to { opacity: 0; transform: scale(1.2); } }

/* -- The markets it watches -- */
.hb-mkt { position: absolute; width: 192px; height: 38px; display: flex; align-items: center; gap: 9px; padding: 0 12px; overflow: hidden; border: 1px solid var(--border-strong); border-radius: 10px; background: var(--surface); box-shadow: 0 0 18px color-mix(in srgb, var(--brand) 12%, transparent); transition: opacity 0.4s ease, transform 0.4s ease; }
.hb.is-armed .hb-mkt:not(.is-shown) { transform: translateY(8px); }
.hb-mkt b { flex: 1; font-size: 12px; font-weight: 600; }
.hb-mkt span { font-size: 10px; color: var(--text-muted); }
.hb-live { width: 7px; height: 7px; border-radius: 50%; background: var(--positive); box-shadow: 0 0 8px var(--positive); }
.hb-scan { position: absolute; left: 0; bottom: 0; width: 60px; height: 2px; background: linear-gradient(90deg, transparent, var(--brand-light)); opacity: 0.6; transform: translateX(-60px); }
.hb-link { position: absolute; height: 1px; background: color-mix(in srgb, var(--brand-light) 28%, transparent); transform-origin: 0 50%; }
.hb-link > i { position: absolute; left: -3px; top: -3px; width: 7px; height: 7px; border-radius: 50%; background: var(--brand-lighter); box-shadow: 0 0 8px var(--brand-light); opacity: 0; }

/* -- The watching loop --
   Transform and opacity only, on small HTML elements, so the compositor runs all of it. It runs only
   while the hero is on screen (`motion.js` adds `is-live`), never on a device the governor has
   stepped down to `motion-still`, and not at all under reduced motion (`is-still`). */
.hb.is-watching.is-live .hb-live { animation: wp-pulse 2s ease-in-out infinite; animation-delay: calc(var(--hb-i, 0) * -0.37s); }
.hb.is-watching.is-live .hb-mkt .hb-live { animation-delay: calc(var(--hb-i) * -0.37s); }
.hb.is-watching.is-live .hb-scan { animation: hb-scan 2.6s linear infinite; animation-delay: calc(var(--hb-i) * -0.83s); }
.hb.is-watching.is-live .hb-mkt .hb-scan { animation-delay: calc(var(--hb-i) * -0.83s); }
.hb.is-watching.is-live .hb-link > i { animation: hb-pulse 1.8s linear infinite; animation-delay: calc(var(--hb-i) * -0.23s); }
.hb.is-watching.is-live .hb-cursor { animation: hb-sweep 3.2s linear infinite; }
.hb.is-watching.is-live .hb-mark::after { animation: hb-ring 3.2s linear infinite; animation-delay: var(--hb-at); }

@keyframes hb-scan { from { transform: translateX(-60px); } to { transform: translateX(192px); } }
@keyframes hb-pulse {
  0% { transform: translateX(0); opacity: 0; }
  15% { opacity: 1; }
  85% { opacity: 1; }
  100% { transform: translateX(var(--hb-len)); opacity: 0; }
}
@keyframes hb-sweep {
  0% { transform: translateX(0); opacity: 0; }
  6% { opacity: 1; }
  94% { opacity: 1; }
  100% { transform: translateX(388px); opacity: 0; }
}
@keyframes hb-ring {
  0% { transform: scale(1); opacity: 0.9; }
  25% { transform: scale(3.4); opacity: 0; }
  100% { transform: scale(3.4); opacity: 0; }
}

.motion-still .hb * { animation-play-state: paused; }
.hb.is-still * { animation: none !important; }

/* -- Web and mobile: a browser window with a phone in front of it ----------------------------- */

/* Static HTML, no animation: the picture is the point (the web app on a wide screen, the phone
   beside it), and the page already has enough moving parts above it. Drawn from tokens so it
   follows the theme like every other product picture here. */
.webapp { position: relative; padding: 0 0 36px; }
.webapp__window {
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 14px; overflow: hidden;
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, .55);
}
.webapp__bar { display: flex; align-items: center; gap: 6px; padding: 10px 12px; border-bottom: 1px solid var(--border); background: var(--surface-raised); }
.webapp__bar span { width: 9px; height: 9px; border-radius: 50%; background: var(--border-strong); }
.webapp__bar em {
  margin-left: 10px; flex: 1; max-width: 220px; font-style: normal; font-family: var(--font-mono); font-size: 10.5px;
  color: var(--text-dim); background: var(--bg); border-radius: 6px; padding: 3px 10px;
}
.webapp__body { display: grid; grid-template-columns: 40px minmax(0, 1fr); min-height: 230px; }
.webapp__rail { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 14px 0; border-right: 1px solid var(--border); }
.webapp__rail b { width: 16px; height: 16px; border-radius: 5px; background: var(--brand); }
.webapp__rail i { width: 14px; height: 14px; border-radius: 4px; background: var(--border-strong); }
.webapp__main { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 12px; }
.webapp__head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.webapp__head strong { font-size: 13px; font-weight: 600; }
.webapp__head small { font-size: 10.5px; color: var(--text-dim); font-family: var(--font-mono); }
.webapp__tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.webapp__tiles span { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 8px; background: var(--surface-raised); border: 1px solid var(--border); }
.webapp__tiles b { font-family: var(--font-mono); font-size: 13px; font-weight: 600; }
.webapp__tiles small { font-size: 10px; color: var(--text-dim); }
.webapp__heat { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 3px; }
.webapp__heat i { aspect-ratio: 1.6; border-radius: 3px; background: var(--brand-light); }
.webapp__phone {
  position: absolute; right: -6px; bottom: 0; width: 132px; padding: 22px 9px 12px;
  display: flex; flex-direction: column; gap: 7px;
  background: var(--bg); border: 1px solid var(--border-strong); border-radius: 22px;
  box-shadow: 0 24px 50px -18px rgba(0, 0, 0, .7), 0 0 0 4px var(--surface);
}
.webapp__notch { position: absolute; top: 8px; left: 50%; width: 38px; height: 6px; margin-left: -19px; border-radius: 3px; background: var(--surface-raised); }
.webapp__alert { display: flex; flex-direction: column; gap: 1px; padding: 7px 8px; border-radius: 9px; background: color-mix(in srgb, var(--brand) 18%, var(--surface)); border: 1px solid color-mix(in srgb, var(--brand) 45%, transparent); }
.webapp__alert b { font-size: 9.5px; font-weight: 600; color: var(--brand-light); letter-spacing: .5px; text-transform: uppercase; }
.webapp__alert span { font-size: 10.5px; color: var(--text); }
.webapp__row { display: flex; flex-direction: column; gap: 4px; padding: 6px 7px; border-radius: 8px; background: var(--surface); }
.webapp__row span { font-size: 9.5px; color: var(--text-bright); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.webapp__row i { display: block; height: 3px; border-radius: 2px; background: var(--bar-fill); }

@media (min-width: 901px) {
  .webapp { padding: 0 0 44px; margin-right: 24px; }
  .webapp__phone { right: -28px; width: 150px; }
}
