/* ===========================================================================
   The hero figure: estate -> four signals -> Omni, as ONE diagram.
   PLUS the shared figure-motion layer that `compose-figure.css` also uses.

   THE FOUR `fig-*` KEYFRAMES AT THE BOTTOM ARE SHARED, and they are named
   `fig-` rather than `hf-` for that reason. `compose-figure.css` animates its
   own elements with them, so there is exactly one copy of the draw-in machinery
   on this page rather than two that have to be kept in step. The conventions
   travel with them:

     --d     animation-delay, set inline in the markup so the order a figure is
             argued in is readable where the shapes are
     --len   a path's real getTotalLength(), for the dashoffset draw-in
     --march a marching-dash period

   If a third figure appears, use these too. If they ever need to leave this
   file, they want their own `figure-motion.css` loaded before both - do not
   copy them.

   Owned by web-ui, loaded after site.css. Its own file because site.css is at
   this project's 500-line limit and this is a self-contained feature - nothing
   here is referenced from anywhere else, and nothing here defines a --p-* value.

   READ THE COMMENT ABOVE THE MARKUP IN index.html FIRST. It records the
   geometry, the reading order the diagram is arguing, and what was rejected.

   WHY ONE DIAGRAM AND NOT TWO
     Jeremy asked for the self-drawing service map and the OTLP signal-flow
     lockup "blended together so it makes more sense". Two graphics stacked is
     what that rejects, so the emit ticks between the bands are load-bearing
     rather than decorative: they are the only thing that makes the lower band
     read as a consequence of the upper one. Delete them and this is two
     pictures again.

   WHAT IS ANIMATED, AND WHAT IS NOT
     Only `opacity`, `transform` and `stroke-dashoffset`. No layout property is
     touched at any point in the timeline, and the SVG carries a fixed `viewBox`
     with `width: 100%; height: auto`, so its box is final on first paint and
     CLS stays 0 while it draws.

   EVERY INTRO ANIMATION RUNS `both`, AND THE BASE STYLE IS THE END STATE
     Two rules that work together, and both matter:

       - `animation-fill-mode: both` back-fills the `from` frame through the
         delay. Without it an element with `animation-delay: .75s` paints at its
         base style for those 750ms, which on a slow first paint reads as the
         whole diagram flashing in complete and then re-animating.
       - the base style is the FINISHED state - drawn, opaque, untranslated -
         and the keyframes travel from hidden to it. So `animation: none` is by
         itself a correct static end state, which is what the reduced-motion
         block at the bottom relies on. If the base style were the hidden state,
         `animation: none` would render an empty box.

   PATH LENGTHS COME FROM THE MARKUP AS `--len`
     Each drawn path carries its own length in a `--len` custom property,
     measured with `getTotalLength()` in the browser and written into the
     markup. Both alternatives misbehave once a path has a curve in it:
     `pathLength` normalisation is honoured inconsistently across engines, and
     the `stroke-dasharray: 1` trick needs `pathLength` to work at all. A wrong
     length does not fail loudly - it shows as a segment that finishes early and
     then jumps, which is easy to read as a timing bug. Re-measure if any `d`
     attribute changes; the measurement command is in the markup comment.
   =========================================================================== */

/* --- The row ------------------------------------------------------------- */

/* A full-span row UNDER both hero columns, not a new child of `.hero`.
   `.hero`'s own centring grid at >=62rem is written against having a single
   child (`.container`), so adding a sibling there would fight it. This is the
   third item in `.hero__grid` instead. */
.hero-figure {
  grid-column: 1 / -1;
  margin-top: clamp(8px, 2vw, 28px);
}

/* HIDDEN BELOW THE HERO'S OWN TWO-COLUMN BREAKPOINT, and this is arithmetic
   rather than taste.

   SVG text scales with the box, so a label's rendered size is
   `font-size * renderedWidth / 1200`. The lane labels are 15 user units:

     viewport 1440  container 1192   15 x 0.993 = 14.9px
     viewport 1280  container 1144   15 x 0.953 = 14.3px
     viewport  992  container  912   15 x 0.760 = 11.4px   <- the floor
     viewport  704  container  648   15 x 0.540 =  8.1px   <- unreadable

   I was asked to hide it below ~44rem (704px). That is too generous: the whole
   range from 44rem to 62rem renders the lane labels between 8 and 11px. Raising
   the font size does not rescue it either - the lanes are 26 user units apart,
   so anything past ~19 units collides with the lane above. The geometry itself
   would have to change, which is a different figure.

   62rem is also where `.hero__grid` collapses to one column, so the figure
   exists exactly where the two-column hero it belongs to exists. Nothing is
   lost: the hero pitch immediately above already says "metrics, logs, traces
   and RUM into Amazon CloudWatch" in prose. */
@media (max-width: 62rem) {
  .hero-figure { display: none; }
}

/* No card, no border, no fill. The page's clean look is restraint and the hero
   has no frame anywhere else in it; one here would read as a figure pasted
   beside the pitch rather than as part of it. */
.hf {
  display: block;
  width: 100%;
  height: auto;
}

/* --- Ink ----------------------------------------------------------------- */

/* Every colour is a --p-* token, so both themes come free and neither can
   disagree with the palette. Measured against the real composited background -
   the figure sits on `--p-bg` with nothing between - see the report. */
.hf text { font-family: var(--p-mono); }

.hf-eyebrow,
.hf-dest-eyebrow {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.16em;
  fill: var(--p-accent);
}
.hf-note,
.hf-lane-label,
.hf-lane-route { fill: var(--p-muted); }
.hf-note { font-size: 13px; letter-spacing: 0.14em; text-anchor: end; }
.hf-lane-label { font-size: 15px; text-anchor: end; }
.hf-lane-route { font-size: 12px; letter-spacing: 0.04em; }

.hf-node-label { font-size: 15px; fill: var(--p-text); text-anchor: middle; }
.hf-dest-name {
  font-family: var(--p-font);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
  fill: var(--p-text);
}
.hf-dest-name--accent { fill: var(--p-accent); }
.hf-dest-item { font-size: 13px; fill: var(--p-muted); }
/* Purely decorative - it separates the name from the list and carries no
   information of its own, so `--p-border` is the right weight here even though
   it would fail 3:1 as a meaning-bearing graphic. */
.hf-dest-rule { fill: none; stroke: var(--p-border); stroke-width: 1; }

/* Boxes and rules. 1.6 user units reads as a hairline at every width the figure
   is shown at, and `--p-accent` rather than `--p-border` because these strokes
   carry meaning: --p-border is 1.3:1 on --p-bg and would fail the 3:1 that a
   meaningful graphic needs.

   Arrowheads are stroked chevrons rather than filled triangles, so an arrow is
   the same hairline weight as the line it terminates - a solid triangle at this
   scale reads heavier than everything else in the figure. They are separate
   elements from their lines rather than a second subpath on the same `d`,
   because the lane rails carry a marching dash pattern and an arrowhead sharing
   that path would march with it. */
.hf-box { fill: none; stroke: var(--p-accent); stroke-width: 1.6; }
.hf-edge,
.hf-tick,
.hf-rail,
.hf-head,
.hf-tip {
  fill: none;
  stroke: var(--p-accent);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* THE ONE UNHEALTHY NODE. A service map where nothing is ever wrong undersells
   a tool whose entire purpose is carrying a fault, so one node is degraded.

   `--p-warning`, not `--p-danger`: I was asked for "amber via --p-danger", and
   those are two different tokens. `--p-danger` is crimson (#b42332 / #ff9aa5);
   the amber is `--p-warning` (#915500 / #f7cf73). Amber is also the right
   meaning - a degradation, not an outage - and it is already this page's
   vocabulary for exactly this state: the captured `proteus doctor` transcript
   below the hero prints its WARN rows in `--p-warning` and its OK rows in
   `--p-success`. Both tokens clear 3:1 on `--p-bg` in both themes, so this was
   a semantic choice and not a contrast rescue.

   The word "degraded" sits beside it because colour alone may not carry
   information (WCAG 1.4.1), and because a colour needs a legend and a word does
   not. Same stroke weight as a healthy node, so it reads as a state rather than
   as an error banner bolted onto the diagram. */
.hf-node--warn .hf-box { stroke: var(--p-warning); }
.hf-node--warn .hf-node-label { fill: var(--p-warning); }
.hf-state { font-size: 12px; letter-spacing: 0.06em; fill: var(--p-warning); }

/* --- The intro timeline -------------------------------------------------- */

/* Delays live in the markup as `--d`, so the order the diagram is argued in is
   readable where the shapes are rather than by cross-referencing a class list
   against this file. Everything lands by ~2.4s. */
/* `.hf-tip` is deliberately absent: a lane's arrowhead sits inside the
   `.hf-lane` group and fades in with it, so animating it as well would run two
   opacity animations over the same pixels for no visible gain. */
.hf-eyebrow,
.hf-note,
.hf-head,
.hf-state,
.hf-lane {
  animation: fig-fade 0.5s ease-out var(--d, 0s) both;
}

.hf-node,
.hf-dest {
  animation: fig-rise 0.55s cubic-bezier(0.22, 0.61, 0.36, 1) var(--d, 0s) both;
}

/* `stroke-dasharray: var(--len)` makes one dash the length of the whole path
   followed by an equal gap, so offset 0 is fully drawn and offset --len is
   fully hidden. That is the whole mechanism. */
.hf-edge,
.hf-tick {
  stroke-dasharray: var(--len);
  stroke-dashoffset: 0;
  animation: fig-draw 0.5s ease-in-out var(--d, 0s) both;
}

/* MARCHING DASHES. `5 9` sums to 14, so travelling exactly -14 lands the
   pattern where it started and the loop has no seam. Any other distance shows
   as a visible jump once per iteration.

   Time-based rather than scroll-driven on purpose: the figure sits above the
   fold and its job is to look live while the visitor is still reading the
   pitch, when there has been no scroll to drive anything.

   The four lanes get different durations AND different delays. Equal durations
   read as one bus with four wires; co-prime-ish periods never re-sync, so they
   read as four independent streams. The delay alone is not enough - it only
   phase-shifts a shared period. */
.hf-rail {
  stroke-dasharray: 5 9;
  stroke-dashoffset: 0;
  animation: fig-march var(--march, 1.3s) linear var(--d, 0s) infinite both;
}

@keyframes fig-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes fig-rise {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fig-draw {
  from { stroke-dashoffset: var(--len); }
  to   { stroke-dashoffset: 0; }
}
@keyframes fig-march {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: -14px; }
}

/* --- Reduced motion ------------------------------------------------------ */

/* A MEANINGFUL STATIC END STATE, not a frozen mid-animation: the base styles
   above are the finished diagram, so switching every animation off renders the
   estate drawn, all four lanes dashed, the fault amber and the destination
   labelled. Nothing is missing and nothing is half-drawn.

   site.css already collapses every animation on the page to 0.001ms under
   `reduce`, which would reach the same frame through `both`. This says it
   directly anyway, because that rule is a global hammer in a file this one does
   not own, and a figure that only renders correctly because of a rule somewhere
   else is a figure that breaks when that rule is narrowed. */
@media (prefers-reduced-motion: reduce) {
  .hf-eyebrow, .hf-note, .hf-head, .hf-state, .hf-lane,
  .hf-node, .hf-dest, .hf-edge, .hf-tick, .hf-rail {
    animation: none;
  }
}
