/* ══════════════════════════════════════════════════════════════════════════════════════════
   piforge.app — homepage hero and product mosaic
   ══════════════════════════════════════════════════════════════════════════════════════════

   Everything here is scoped under #pf-lp and every keyframe is prefixed pflp-. That is not
   tidiness: this page also loads Tailwind from a CDN, Tailwind ships a keyframe called `ping`,
   and the schematic layer had one too. Whichever stylesheet loaded last would have won, and the
   alarm ring would have animated the wrong property with nothing to show for it in the source.

   The mosaic is five tiles. Each one draws a schematic of what a product does, keeps that drawing
   in motion, then dissolves into a screenshot of the real thing and back again — on its own phase,
   so the grid never flips as a block. Pointing at a tile holds it. */

/* ── palette ────────────────────────────────────────────────────────────────────────────────
     Near-black navy ground, an electric blue used at FULL BLEED for whole sections, white bands for
     relief. PIForge keeps four hues for one job only — the streams in the schematics and the family
     stripe in the index — so the code that makes the catalogue readable survives inside the
     borrowed register instead of fighting it. */
#pf-lp {
    --navy:   #051c2c;
    --navy-2: #0a2739;
    --sheet:  #041823;   /* the drawing sheet */
    --blue:   #2251ff;
    --blue-d: #1a41d8;
    --white:  #ffffff;
    --ink:    #ffffff;
    --slate:  #a9bccb;
    --rule:   #17384c;

    --teal:   #2dd4bf;   /* material & flow */
    --ochre:  #e0b155;   /* time & events */
    --clay:   #ef8a6a;   /* limits & alarms */
    --indigo: #86b0dd;   /* signal */
    --focus:  #6d8dff;

    /* Aptos first, as asked. It is Microsoft 365's face and ships with recent Office, so most
       readers on Windows get it; the rest fall through to the nearest grotesques rather than to a
       serif, which keeps the page's proportions intact either way. */
    --sans: Aptos, "Aptos Display", "Segoe UI Variable", "Segoe UI",
            -apple-system, BlinkMacSystemFont, system-ui, Helvetica, Arial, sans-serif;
    --mono: "Cascadia Mono", Consolas, "SF Mono", ui-monospace, monospace;
  }

#pf-lp { background: var(--navy); color: var(--ink); font-family: var(--sans);
         font-size: 16px; line-height: 1.55; -webkit-font-smoothing: antialiased;
         /* 96 clears the fixed header; the tail is trimmed so the section below reaches into the
            first screen instead of starting exactly where it ends. */
         padding: 96px 0 36px; }
#pf-lp *, #pf-lp *::before, #pf-lp *::after { box-sizing: border-box; }
#pf-lp p { margin: 0; }
#pf-lp a { color: inherit; text-decoration: none; }
#pf-lp :focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
@media (max-width: 760px) { #pf-lp { padding: 88px 0 52px; } }
/* The hero and the mosaic sit in the same band as every Tailwind section below them, and they get
   the width from the same place: --pf-band / --pf-gutter in style.css. This used to be a flat
   1240px with 34px of padding, which lined up near enough on a laptop and fell apart above it —
   style.css widens its bands at 1600px, 2100px and 2800px, and the hero did not follow, so the top
   of the page sat 300px narrower than the rest at 1920 and 854px narrower at 3440. The fallbacks
   are only for the case where style.css has not loaded. */
#pf-lp .spine { max-width: var(--pf-band, 80rem); margin: 0 auto;
                padding: 0 var(--pf-gutter, 1.5rem); }
@media (max-width: 560px) {
    #pf-lp .spine { padding: 0 20px; }
  }
#pf-lp .kicker { display: block; font-size: 11px; font-weight: 700; letter-spacing: .14em;
            text-transform: uppercase; color: var(--white); margin-bottom: 8px; }
#pf-lp .kicker.t { color: var(--teal); }
#pf-lp .kicker.o { color: var(--ochre); }
#pf-lp .kicker.c { color: var(--clay); }
#pf-lp .kicker.i { color: var(--indigo); }
#pf-lp h1 { font-weight: 600; font-size: clamp(2.4rem, 4.6vw, 3.7rem); line-height: 1.06;
       letter-spacing: -.028em; margin: 0; text-wrap: balance; }
#pf-lp h2 { font-weight: 600; font-size: clamp(1.7rem, 2.6vw, 2.25rem); line-height: 1.16;
       letter-spacing: -.022em; margin: 0; text-wrap: balance; }
#pf-lp h3 { font-weight: 600; font-size: 1.1rem; line-height: 1.28; margin: 0;
       letter-spacing: -.008em; text-wrap: balance; }
#pf-lp .lede { color: var(--slate); font-size: 1rem; line-height: 1.62; max-width: 40em; }
/* ── hero copy ──────────────────────────────────────────────────────────────────────────── */
/* One column, not two. The right-hand cell held a drawing title block; with that gone a grid
     would leave it empty, and an empty cell is exactly what reads as an unfinished layout. */
#pf-lp .hero .lede { margin-top: 22px; max-width: 44em; }
/* ── the hero ───────────────────────────────────────────────────────────────────────────────
     The old version was a headline, a paragraph and a bare arrow, all of which faded in together.
     It read as a caption. This one is staged: an eyebrow with a rule that draws itself, a headline
     whose two lines rise out of their own clipping boxes, then the turn of the argument, then the
     detail, then somewhere to go. Every step is a one-shot reveal that settles and stops — a
     looping headline used to live here and was taken out for good reason. The single thing that
     goes on repeating is the hairline under the claim, and it repeats precisely because it cannot
     reproduce that failure: no glyph is ever mid-anything. See `.turn .hi::after`. */
#pf-lp .hero .eyebrow { display: flex; align-items: center; gap: 14px;
                   font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
                   text-transform: uppercase; color: var(--teal); margin: 0 0 15px;
                   opacity: 0; animation: pflp-appear .6s ease-out .05s forwards; }
#pf-lp .hero .eyebrow::after { content: ""; height: 1px; flex: 1; max-width: 190px;
                          background: currentColor; transform: scaleX(0); transform-origin: left;
                          animation: pflp-rule .8s cubic-bezier(.16,1,.3,1) .35s forwards; }
#pf-lp .hero { padding-bottom: 18px; }
/* The headline is restrained on purpose: nothing sweeps across it, nothing is drawn as a flourish.
     Each line simply rises into place out of its own clipping box, in sequence, and stops. That is
     the entire effect — a block that covered the words and then retracted was more theatre than a
     page arguing for calm and directness can carry. The one continuing motion in the hero is kept
     off the type entirely, a pixel of rule under the claim two lines below. */
#pf-lp .hero h1 { font-size: clamp(1.9rem, 4.15vw, 3.7rem); line-height: 1.16; letter-spacing: -.035em; }
#pf-lp .hero .ln { display: block; overflow: hidden; padding-bottom: .1em; }
#pf-lp .hero .ln > span { display: block; opacity: 0; transform: translateY(46%);
                     animation: pflp-lift .7s cubic-bezier(.19,1,.22,1) .18s forwards,
                                pflp-appear .01s linear .18s forwards; }
/* Set against the right edge, under a left-aligned headline: the asymmetry is the point, and it
     is what makes a right-aligned line read as a considered layout choice rather than a mistake.
     The reveal still clips left to right — reading order — even though the block itself sits at
     the right margin; revealing right to left had the words arrive out of reading order, which is
     what made the first version feel backwards rather than deliberate.
     Colour appears exactly once, on the phrase that is the actual claim: teal is the one accent
     this whole site commits to (the logo owns it — see style.css's "one accent" note), so reusing
     it here rather than reaching for a second colour keeps this line inside that discipline. */
#pf-lp .hero .turn { margin: 16px 0 0; text-align: right; font-size: clamp(1rem, 1.4vw, 1.22rem);
                font-weight: 400; line-height: 1.4; color: var(--slate); }
/* The wipe animates `clip-path` and `letter-spacing` together, so the box is still resizing while
     it is being clipped — and `.turn` is right-aligned, so it shifts as it shrinks. Chromium 86 and
     older size the promoted `will-change: clip-path` layer once and never re-resolve the inset
     against the settled box, which leaves the last few words permanently cut off. Gating the effect
     on `aspect-ratio` support is really a gate on engine era (Chromium 88+, Safari 15+, Firefox 89+
     — none of which have the bug); older engines simply get the line, unanimated and complete. */
#pf-lp .hero .turn .reveal { display: inline-block; }
@supports (aspect-ratio: 1 / 1) {
    /* `backwards`, not `both`. Holding the last frame leaves `clip-path: inset(0 0% 0 0)` on the
       element forever — geometrically a no-op, but still a clip, and it silently eats anything a
       descendant paints outside the border box (the rule under the claim was invisible for exactly
       this reason). The final frame is identical to the unanimated base style, so dropping the
       forwards fill changes nothing on screen and gives the box its overflow back. */
    #pf-lp .hero .turn .reveal { will-change: clip-path;
                            animation: pflp-turnwipe .95s cubic-bezier(.19,1,.22,1) .58s backwards; }
  }
/* The one thing in the hero that keeps moving after everything has settled. The rule against a
     looping hero still holds where it was written — the headline used to retype itself and a
     visitor arriving mid-cycle met a half-finished word — so the loop here is built to be immune to
     that failure: it never touches a glyph. The words are painted once and stay painted; all that
     repeats is a hairline under the claim, drawn left to right in reading order, held, then eaten
     from the same side so the motion never reverses. Arrive at any point in the seven seconds and
     the sentence is whole. Amplitude is one pixel and the colour is the teal already on the phrase,
     which keeps it inside the "one accent" discipline rather than adding a second gesture. */
#pf-lp .hero .turn .hi { position: relative; display: inline-block;
                    color: var(--teal); font-weight: 600; }
/* Hung below the box on purpose. At this size the line box leaves about a pixel under the glyphs —
     not enough to clear the descender of the "p", which a rule placed inside would run straight
     through. It only has room to sit outside because the wipe above no longer holds a clip on the
     parent. */
#pf-lp .hero .turn .hi::after { content: ""; position: absolute; left: 0; right: 0; bottom: -.16em;
                           height: 1px; background: currentColor; opacity: .5;
                           transform: scaleX(0); transform-origin: left;
                           animation: pflp-claimrule 7s linear 1.9s infinite; }
/* scaleX is still 1 when the origin flips, so the switch itself paints nothing — it only decides
     which end the retraction collapses toward. */
/* The eyebrow rule next to the kicker uses cubic-bezier(.16,1,.3,1) — right for a one-shot that
     should feel decisive, wrong here. Front-loaded that hard, the draw is visually over in a third
     of its own segment, which on a loop reads as a flicker rather than a movement. These two are
     gentler and asymmetric: the line eases out as it arrives and settles, and eases in as it leaves.
     Roughly 1.3s to draw, 2.4s held, 1.3s to retract, 2s of nothing. */
@keyframes pflp-claimrule {
     0%     { transform: scaleX(0); transform-origin: left;
              animation-timing-function: cubic-bezier(.25,.6,.3,1); }
    18%     { transform: scaleX(1); transform-origin: left;  }
    52%     { transform: scaleX(1); transform-origin: left;  }
    52.01%  { transform: scaleX(1); transform-origin: right;
              animation-timing-function: cubic-bezier(.6,.04,.72,.36); }
    70%     { transform: scaleX(0); transform-origin: right; }
   100%     { transform: scaleX(0); transform-origin: right; }
  }
@keyframes pflp-turnwipe {
    from { clip-path: inset(0 100% 0 0); opacity: .001; letter-spacing: .1em; }
    to   { clip-path: inset(0 0% 0 0);   opacity: 1;    letter-spacing: normal; }
  }
@keyframes pflp-appear { to { opacity: 1; } }
@keyframes pflp-rule { to { transform: scaleX(1); } }
@keyframes pflp-lift { to { transform: none; } }
/* ── the mosaic ─────────────────────────────────────────────────────────────────────────────
     Deliberately uneven and deliberately off-register: one tall plate, then two staggered columns
     beside it. Every tile is a drawing that keeps resolving into the screen it came from, so the
     grid never sits still and never flips all at once — each tile runs on its own phase. */
#pf-lp .mosaic { display: grid; grid-template-columns: repeat(12, 1fr); gap: 18px;
            align-items: start; }
#pf-lp .m-hero { grid-column: 8 / -1; grid-row: 1 / span 2; align-self: stretch; }
/* Flatter than they were (4/3, 1/1, 3/2, 4/3 with a 54px stagger). Those ratios put the mosaic at
   711px on a 2560 screen, and with the headline above it the hero filled the whole first screen —
   the next section began at 1051px and never showed, so the page gave no sign that anything
   followed it. Landscape tiles suit the content anyway: every `.real` layer is a screenshot of a
   wide application window, which was letterboxing top and bottom inside a tall box. The stagger is
   still there, just enough to keep the two columns off-register. */
#pf-lp .m-a { grid-column: 1 / span 4; aspect-ratio: 16 / 9; }
#pf-lp .m-b { grid-column: 5 / span 3; aspect-ratio: 4 / 3;   margin-top: 24px; }
#pf-lp .m-c { grid-column: 1 / span 4; aspect-ratio: 2 / 1; }
#pf-lp .m-d { grid-column: 5 / span 3; aspect-ratio: 16 / 10; margin-top: 24px; }
@media (max-width: 1000px) {
    #pf-lp .mosaic { grid-template-columns: repeat(6, 1fr); }
    #pf-lp .m-hero { grid-column: 1 / -1; grid-row: auto; aspect-ratio: 4 / 5; }
    #pf-lp .m-a, #pf-lp .m-c { grid-column: 1 / span 3; }
    #pf-lp .m-b, #pf-lp .m-d { grid-column: 4 / span 3; margin-top: 0; }
  }
@media (max-width: 620px) {
    #pf-lp .mosaic { grid-template-columns: 1fr; }
    #pf-lp .m-hero { aspect-ratio: 3 / 4; }
    #pf-lp .m-a, #pf-lp .m-b, #pf-lp .m-c, #pf-lp .m-d { grid-column: 1 / -1; aspect-ratio: 4 / 3; }
  }
/* A tile takes its entire height from `aspect-ratio` — every child inside it is absolutely
     positioned, so nothing else holds the box open. On an engine without that property the tile
     collapses to its 1px border and the mosaic reads as four hairlines under the headline. That is
     not a hypothetical audience: PI Vision runs on locked-down plant workstations whose browser was
     frozen years ago, and those are the machines an engineer browses this site from. The ratio
     padding reproduces the same boxes; engines that do support `aspect-ratio` never read the block. */
@supports not (aspect-ratio: 1 / 1) {
    /* Must track the aspect-ratio values above — they are the same shapes written twice. */
    #pf-lp .m-a { padding-top: 56.25%; }     /* 16 / 9  */
    #pf-lp .m-b { padding-top: 75%; }        /*  4 / 3  */
    #pf-lp .m-c { padding-top: 50%; }        /*  2 / 1  */
    #pf-lp .m-d { padding-top: 62.5%; }      /* 16 / 10 */
    @media (max-width: 1000px) {
        #pf-lp .m-hero { padding-top: 125%; }        /* 4 / 5 */
      }
    @media (max-width: 620px) {
        #pf-lp .m-hero { padding-top: 133.3333%; }   /* 3 / 4 */
        #pf-lp .m-a, #pf-lp .m-b, #pf-lp .m-c, #pf-lp .m-d { padding-top: 75%; }
      }
  }
#pf-lp .tile { position: relative; display: block; overflow: hidden;
          border: 1px solid var(--rule); background: var(--sheet); }
/* Longhands first, `inset` second: same values either way, so this only matters to the engines
     that predate the shorthand — without them the absolutely positioned layers below never fill
     the tile. Same reason on .real, .scrim and .foot. */
#pf-lp .tile .stage { position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0; }
#pf-lp .tile svg { display: block; width: 100%; height: 100%;
              transition: opacity .8s ease; }
/* `contain`, not `cover`. Every screenshot here is a wide application window, and the tiles are
     square or taller — cropping one to fill leaves a vertical sliver of a dialog, which is what
     made the earlier version unreadable. Letterboxed against the sheet colour it reads as a framed
     screen, and sitting high in the tile keeps it clear of the caption band. */
#pf-lp .tile .real { position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0;
                width: 100%; height: 100%;
                object-fit: contain; object-position: center 30%;
                background: var(--sheet);   /* same ground as the drawing, so the crossfade does not
                                              flash a different colour behind the two views */
                opacity: 0; transition: opacity .8s ease; }
#pf-lp .tile.is-real svg { opacity: 0; }
#pf-lp .tile.is-real .real { opacity: 1; }
/* The words sit on the picture, so the scrim has to be heavy enough to hold them over a light
     screenshot as well as over the drawing — and the drawings are laid out to keep their content
     clear of this band. */
#pf-lp .tile .scrim { position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0;
                 pointer-events: none;
                 background: linear-gradient(to top,
                   rgba(4,18,28,.97) 0%, rgba(4,18,28,.90) 16%,
                   rgba(4,18,28,.55) 34%, rgba(4,18,28,0) 56%); }
#pf-lp .tile .foot { position: absolute; top: auto; right: 0; bottom: 0; left: 0;
                inset: auto 0 0 0; padding: 18px 20px 18px; }
#pf-lp .m-hero .foot { padding: 26px 26px 26px; }
#pf-lp .m-hero h3 { font-size: 1.42rem; }
#pf-lp .m-b h3, #pf-lp .m-d h3 { font-size: .98rem; }
/* which of the two the reader is looking at, said plainly */
#pf-lp .chip { position: absolute; top: 12px; left: 14px; z-index: 2;
          font-family: var(--mono); font-size: 9.5px; letter-spacing: .1em;
          text-transform: uppercase; color: var(--slate);
          border: 1px solid var(--rule); background: rgba(4,18,28,.7);
          padding: 3px 7px; }
#pf-lp .tile.is-real .chip { color: var(--white); border-color: rgba(255,255,255,.3); }
#pf-lp .chip::after { content: "schematic"; }
#pf-lp .tile.is-real .chip::after { content: "live screen"; }
#pf-lp .tile:hover .chip { border-color: var(--blue); }
/* ── the drawing animations ────────────────────────────────────────────────────────────────
     Everything starts paused, so a drawing further down the page has not already played itself out
     before the reader arrives. The gate has to out-specify the per-element rules below, because the
     `animation` shorthand resets play-state to running and a plain `.fig-anim *` rule loses to
     every one of them. */
#pf-lp .tile:not(.is-playing) .fig-anim * { animation-play-state: paused; }
@keyframes pflp-fade { to { opacity: 1; } }
@keyframes pflp-show { to { opacity: .95; } }
@keyframes pflp-draw { to { stroke-dashoffset: 0; } }
@keyframes pflp-drift { to { stroke-dashoffset: -24; } }
@keyframes pflp-breathe { 0%, 100% { opacity: .95; } 50% { opacity: .35; } }
@keyframes pflp-ping { 0% { r: 3; opacity: 1; } 70%, 100% { r: 10; opacity: 0; } }
/* PIFlow — the plant is understood vessel by vessel, then stream by stream */
#pf-lp .fig-anim .vessel { opacity: 0; animation: pflp-fade .45s ease-out forwards; }
#pf-lp .fig-anim .vessel:nth-of-type(1) { animation-delay: .08s; }
#pf-lp .fig-anim .vessel:nth-of-type(2) { animation-delay: .16s; }
#pf-lp .fig-anim .vessel:nth-of-type(3) { animation-delay: .24s; }
#pf-lp .fig-anim .vessel:nth-of-type(4) { animation-delay: .32s; }
#pf-lp .fig-anim .vessel:nth-of-type(5) { animation-delay: .40s; }
#pf-lp .fig-anim .stream { stroke-dasharray: 100; stroke-dashoffset: 100;
                      animation: pflp-draw .8s cubic-bezier(.4,0,.2,1) forwards; }
#pf-lp .fig-anim .s1 { animation-delay: .55s; }
#pf-lp .fig-anim .s2 { animation-delay: .66s; }
#pf-lp .fig-anim .s3 { animation-delay: 1.05s; }
#pf-lp .fig-anim .s4 { animation-delay: 1.16s; }
#pf-lp .fig-anim .s5 { animation-delay: 1.55s; }
#pf-lp .fig-anim .flow { stroke-dasharray: 3 9; opacity: 0;
                    animation: pflp-show .5s linear 2.0s forwards, pflp-drift 2.6s linear 2.0s infinite; }
#pf-lp .fig-anim .tip { opacity: 0; animation: pflp-fade .3s ease-out forwards; }
#pf-lp .fig-anim .t1, #pf-lp .fig-anim .t2 { animation-delay: 1.30s; }
#pf-lp .fig-anim .t3, #pf-lp .fig-anim .t4 { animation-delay: 1.80s; }
#pf-lp .fig-anim .t5 { animation-delay: 2.30s; }
#pf-lp .fig-anim .signal { stroke-dasharray: 100; stroke-dashoffset: 100;
                      animation: pflp-draw .7s ease-out 1.9s forwards; }
#pf-lp .fig-anim .signal-dot { opacity: 0; animation: pflp-fade .35s ease-out 2.5s forwards; }
#pf-lp .fig-anim .draft { opacity: 0; animation: pflp-show .5s ease-out 2.7s forwards,
                                            pflp-breathe 2.6s ease-in-out 3.2s infinite; }
#pf-lp .fig-anim .draft-label { opacity: 0; animation: pflp-fade .45s ease-out 2.9s forwards; }
#pf-lp .fig-anim .tag { opacity: 0; animation: pflp-fade .45s ease-out .75s forwards; }
/* Sankey — the bands fill from source to sink, so width reads as quantity */
#pf-lp .fig-anim .band { stroke-dasharray: 100; stroke-dashoffset: 100;
                    animation: pflp-draw 1.0s cubic-bezier(.4,0,.2,1) forwards; }
#pf-lp .fig-anim .b1 { animation-delay: .30s; }
#pf-lp .fig-anim .b2 { animation-delay: .44s; }
#pf-lp .fig-anim .b3 { animation-delay: 1.10s; }
#pf-lp .fig-anim .b4 { animation-delay: 1.24s; }
#pf-lp .fig-anim .node { opacity: 0; animation: pflp-fade .45s ease-out .08s forwards; }
#pf-lp .fig-anim .bar { opacity: 0; animation: pflp-fade .45s ease-out .95s forwards; }
#pf-lp .fig-anim .num { opacity: 0; animation: pflp-fade .45s ease-out forwards; }
#pf-lp .fig-anim .n1 { animation-delay: .80s; }
#pf-lp .fig-anim .n2 { animation-delay: .90s; }
#pf-lp .fig-anim .n3 { animation-delay: 1.45s; }
#pf-lp .fig-anim .n4 { animation-delay: 1.90s; }
#pf-lp .fig-anim .n5 { animation-delay: 2.05s; }
/* AlarmIQ — the flood arrives in time order, which is the whole point */
#pf-lp .fig-anim .lane { opacity: 0; animation: pflp-fade .35s ease-out forwards; }
#pf-lp .fig-anim .l1 { animation-delay: .08s; }
#pf-lp .fig-anim .l2 { animation-delay: .16s; }
#pf-lp .fig-anim .l3 { animation-delay: .24s; }
#pf-lp .fig-anim .l4 { animation-delay: .32s; }
#pf-lp .fig-anim .chat { opacity: 0; animation: pflp-fade .28s ease-out forwards,
                                           pflp-breathe 1.3s ease-in-out infinite; }
#pf-lp .fig-anim .chat:nth-of-type(odd) { animation-delay: .65s, 2.1s; }
#pf-lp .fig-anim .chat:nth-of-type(even) { animation-delay: .85s, 2.4s; }
#pf-lp .fig-anim .ev { opacity: 0; animation: pflp-fade .3s ease-out forwards; }
#pf-lp .fig-anim .e1 { animation-delay: 1.45s; }
#pf-lp .fig-anim .e2 { animation-delay: 1.75s; }
#pf-lp .fig-anim .e3 { animation-delay: 2.00s; }
/* this one fades rather than draws: the draw animation sets stroke-dasharray in CSS, which beats
     the presentation attribute and would flatten the dashes into a solid rule — and a solid rule
     reads as a limit, not as "the flood starts here" */
#pf-lp .fig-anim .flood-line { opacity: 0; animation: pflp-fade .4s ease-out 2.25s forwards; }
#pf-lp .fig-anim .verdict { opacity: 0; animation: pflp-fade .45s ease-out 2.6s forwards; }
#pf-lp .fig-anim .verdict-ring { opacity: 0; animation: pflp-ping 2.1s ease-out 2.9s infinite; }
/* Correlation — the cloud arrives, then the line it implies, then the number */
#pf-lp .fig-anim .frame { opacity: 0; animation: pflp-fade .4s ease-out .08s forwards; }
#pf-lp .fig-anim .cloud { opacity: 0; animation: pflp-fade .9s ease-out .45s forwards; }
#pf-lp .fig-anim .fit { stroke-dasharray: 100; stroke-dashoffset: 100;
                     animation: pflp-draw 1.0s cubic-bezier(.4,0,.2,1) 1.25s forwards; }
#pf-lp .fig-anim .stat { opacity: 0; animation: pflp-fade .45s ease-out 2.15s forwards; }
/* Gantt — the shift fills in from the start of the window */
#pf-lp .fig-anim .track { opacity: 0; animation: pflp-fade .35s ease-out forwards; }
#pf-lp .fig-anim .k1 { animation-delay: .08s; }
#pf-lp .fig-anim .k2 { animation-delay: .16s; }
#pf-lp .fig-anim .k3 { animation-delay: .24s; }
#pf-lp .fig-anim .blk { opacity: 0; animation: pflp-fade .3s ease-out forwards; }
#pf-lp .fig-anim .blk:nth-of-type(1) { animation-delay: .50s; }
#pf-lp .fig-anim .blk:nth-of-type(2) { animation-delay: .70s; }
#pf-lp .fig-anim .blk:nth-of-type(3) { animation-delay: .90s; }
#pf-lp .fig-anim .blk:nth-of-type(4) { animation-delay: 1.10s; }
#pf-lp .fig-anim .blk:nth-of-type(5) { animation-delay: 1.30s; }
#pf-lp .fig-anim .axis { opacity: 0; animation: pflp-fade .4s ease-out 1.55s forwards; }
/* ── the permanent layer ───────────────────────────────────────────────────────────────────
     The reveal builds the drawing; this keeps it alive afterwards. Without it each tile assembled
     itself once and then sat perfectly still until the crossfade came round again, which read as
     "the animation stopped".

     A packet is one short dash chased along a normalised path: with pathLength="100" and a
     6-and-100 pattern, driving the offset from 106 to 0 walks that dash from the start of the line
     clean off the end, once per cycle, for ever. */
#pf-lp .fig-anim .packet { stroke-dasharray: 6 100; stroke-dashoffset: 106; opacity: 0;
                      animation: pflp-fade .4s linear 1.9s forwards,
                                 pflp-packet 2.1s cubic-bezier(.45,0,.55,1) 1.9s infinite; }
#pf-lp .fig-anim .packet.p1 { animation-delay: 1.90s, 1.90s; }
#pf-lp .fig-anim .packet.p2 { animation-delay: 2.05s, 2.05s; }
#pf-lp .fig-anim .packet.p3 { animation-delay: 2.35s, 2.35s; }
#pf-lp .fig-anim .packet.p4 { animation-delay: 2.50s, 2.50s; }
#pf-lp .fig-anim .packet.p5 { animation-delay: 2.80s, 2.80s; }
@keyframes pflp-packet { from { stroke-dashoffset: 106; } to { stroke-dashoffset: 0; } }
/* the process node keeps taking the load */
#pf-lp .fig-anim .throb { animation: pflp-fade .45s ease-out .95s forwards, pflp-throb 2.4s ease-in-out 1.6s infinite; }
@keyframes pflp-throb { 0%, 100% { opacity: 1; } 50% { opacity: .62; } }
/* the timelines keep being re-evaluated, so the scan keeps crossing them */
#pf-lp .fig-anim .scan { transform-box: view-box; transform-origin: 0 0; opacity: 0;
                    animation: pflp-fade .4s linear 2.4s forwards, pflp-scan 4.2s linear 2.4s infinite; }
#pf-lp .fig-anim .scan.g { animation: pflp-fade .4s linear 1.8s forwards, pflp-scanG 5.4s linear 1.8s infinite; }
@keyframes pflp-scan { from { transform: translateX(0); } to { transform: translateX(336px); } }
@keyframes pflp-scanG { from { transform: translateX(0); } to { transform: translateX(332px); } }
/* the correlation window slides the way the symbol computes it */
#pf-lp .fig-anim .roll { transform-box: view-box; transform-origin: 0 0; opacity: 0;
                    animation: pflp-fade .5s linear 2.3s forwards, pflp-roll 5.6s ease-in-out 2.3s infinite; }
@keyframes pflp-roll { 0% { transform: translateX(0); } 50% { transform: translateX(238px); }
                    100% { transform: translateX(0); } }
#pf-lp .fig-anim .cloud circle { animation: pflp-twinkle 2.8s ease-in-out infinite; }
#pf-lp .fig-anim .cloud circle:nth-child(3n) { animation-delay: .5s; }
#pf-lp .fig-anim .cloud circle:nth-child(3n+1) { animation-delay: 1.2s; }
@keyframes pflp-twinkle { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
/* ── entrances with some snap ──────────────────────────────────────────────────────────────
     Plain opacity fades made a five-tile grid feel inert. Blocks now arrive with direction: the
     alarm cascade slides in from where its time axis starts, and the shift wipes across. */
#pf-lp .fig-anim .ev, #pf-lp .fig-anim .blk { transform-box: fill-box; }
#pf-lp .fig-anim .ev { animation: pflp-slidein .45s cubic-bezier(.16,1,.3,1) forwards; }
#pf-lp .fig-anim .blk { transform-origin: left center;
                   animation: pflp-wipein .4s cubic-bezier(.16,1,.3,1) forwards; }
@keyframes pflp-slidein { from { opacity: 0; transform: translateX(-16px); }
                       to   { opacity: 1; transform: none; } }
@keyframes pflp-wipein { from { opacity: 0; transform: scaleX(0); }
                       to   { opacity: 1; transform: scaleX(1); } }
/* the signal keeps reporting */
#pf-lp .fig-anim .signal-dot { animation: pflp-fade .35s ease-out 2.5s forwards,
                                     pflp-throb 1.8s ease-in-out 3.0s infinite; }
/* ── reduced motion ────────────────────────────────────────────────────────────────────────
     Two goes at this were wrong in the same direction. First it switched the drawings off entirely,
     so on Windows Server — where "show animations" is off by default and Chrome therefore reports
     `reduce` — the schematics never played at all. Then it kept the reveal but killed everything
     that repeats, so they played once and froze, which is the same complaint again.

     The page's whole argument is the drawing working, so here the flag makes the motion CALMER
     rather than absent. What is left is small-amplitude by construction: a short dash travelling a
     thin line, a one-pixel scan, an opacity breath — no large moving areas, no parallax. Each runs
     at roughly half speed. The one genuinely loud effect, a ring that grows outward, is dropped. */
@media (prefers-reduced-motion: reduce) {
    #pf-lp .rise { animation: none; }
    /* Slower, not absent. A stray full reset used to sit right after this — "animation: none" on
       the same .ln > span selector — which zeroes animation-name and beats a duration-only rule
       regardless of source order. That is how the headline ended up appearing instantly on a
       machine that reports this flag by default (Windows Server, "show animations" off) even
       after the durations here were lengthened: the later full reset always won. */
    #pf-lp .hero .eyebrow { animation-duration: 1.1s; }
    #pf-lp .hero .eyebrow::after { animation-duration: 1.6s; }
    #pf-lp .hero .ln > span { animation-duration: 1.3s, .01s; }
    #pf-lp .hero .turn .reveal { animation-duration: 1.7s; }
    /* The one looping thing in the hero, and the only one here that never stops on its own — so it
       gets the largest slowdown of anything in this block. A one-pixel rule at this pace is the
       "opacity breath" end of the scale described above, not a moving area. */
    #pf-lp .hero .turn .hi::after { animation-duration: 13s; }
    #pf-lp .fig-anim .packet { animation-duration: .4s, 4.4s; }
    #pf-lp .fig-anim .flow { animation-duration: .5s, 5.4s; }
    #pf-lp .fig-anim .draft { animation-duration: .5s, 5.6s; }
    #pf-lp .fig-anim .chat { animation-duration: .28s, 3.0s; }
    #pf-lp .fig-anim .scan { animation-duration: .4s, 9.0s; }
    #pf-lp .fig-anim .scan.g { animation-duration: .4s, 11.0s; }
    #pf-lp .fig-anim .roll { animation-duration: .5s, 12.0s; }
    #pf-lp .fig-anim .throb, #pf-lp .fig-anim .signal-dot { animation-duration: .45s, 5.0s; }
    #pf-lp .fig-anim .cloud circle { animation-duration: 6.0s; }
    #pf-lp .fig-anim .verdict-ring { display: none; }
  }
#pf-lp .arrow-btn { display: inline-flex; align-items: center; justify-content: center;
               margin-top: 26px; width: 56px; height: 56px; border-radius: 50%;
               background: rgba(255,255,255,.12); color: var(--white); font-size: 1.25rem;
               transition: background .18s ease, transform .18s ease; }
#pf-lp .arrow-btn:hover { background: var(--blue); transform: translateX(3px); }
@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
@keyframes blip { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@keyframes pflp-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
#pf-lp .rise { animation: pflp-rise .7s cubic-bezier(.16,1,.3,1) both; }
#pf-lp .d1 { animation-delay: .05s; }
#pf-lp .d2 { animation-delay: .14s; }
#pf-lp .d3 { animation-delay: .23s; }
@media (prefers-reduced-motion: reduce) {
    #pf-lp .rise { animation: none; }
    #pf-lp .arrow-btn { transition: none; }
  }
