/* The landing intro — a diagram that plays the workflow it describes, then
   hands over to the headline.

   Forked from the app's assets/data_flow.css, which draws the same picture
   behind /flow. Two differences and no others: the star field lives on the body
   here so it continues down the page, and where the app reveals a sign-in card
   out of the hub this reveals the headline and its calls to action.

   Geometry partner of webpage/flow_diagram.py: the aspect-ratio on
   .pim-flow-stage must match that module's viewBox, or the icon tiles stop
   lining up with the curves that reach them.

   Timing partner too. Every animation-delay and animation-duration arrives as
   an inline style from that module's T_* choreography constants; the rules here
   own only the names, easings and iteration counts. Do not add delays here.

   Hidden states live in the keyframes, never in the base rules, and the
   animations run with animation-fill-mode: both. That inverts the usual trap:
   `animation: none` (reduced motion, mobile, skip) leaves the finished diagram
   on screen rather than an invisible one. */

/* The rest of the site moved off Orbitron and onto Inter, because a reviewer
   read the page as too technical to sit down and read. This file is the one
   exception, and it is the only place --pim-font-brand appears outside the
   wordmark: the labels in here are "ERP", "XLSX", "PDF" on machine tiles, four
   letters of tracked-out uppercase inside a picture. They are part of the
   drawing, not part of the prose, and swapping the face would quietly make the
   fork stop matching the app's /flow page it was lifted from. */

/* Except in Bulgarian, where the face has to be swapped: Orbitron has no
   Cyrillic and upstream has none to add, so every label in here would fall
   through to the system UI font -- and the pills and the motto are the one part
   of this diagram that really is prose ("Taxonomies", "One catalog"), so they
   translate. Tektur stands in: same squarish machine-label character, Cyrillic
   included, and the Bulgarian locl forms with it. Scoped to .pim-flow rather
   than :root so the header wordmark, whose inner letter spans also read
   --pim-font-brand, is left in the face it already uses. */
.pim-flow:lang(bg) {
  --pim-font-brand: "PIM Display BG", ui-sans-serif, system-ui, sans-serif;
}

.pim-flow {
  position: relative;
  isolation: isolate;
  /* The header is sticky and 68px tall; the intro fills what is left, so the
     hand-off lands on a full screen and the next section starts below it. */
  min-height: calc(100vh - 68px);
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3rem 1.5rem 5rem;
  overflow: hidden;
  background: radial-gradient(ellipse 46% 40% at 50% 45%, rgba(56, 132, 255, 0.22), transparent 70%);
}

/* The stage: viewBox units and HTML percentages describe the same points,
   because the aspect ratio here matches the SVG viewBox exactly. The container
   context is also what lets the badges express their travel in cqw — one
   percent of the stage — so they land on the hub at every size. */
.pim-flow-stage {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 1160px;
  aspect-ratio: 1160 / 640;
  container-type: inline-size;
  /* Finished state: the diagram has handed over to the headline. The animation
     below runs *from* clear *to* here, so reduced motion and the skip class
     both settle on the handed-over view with one `animation: none`. */
  filter: blur(2px) brightness(0.42);
  animation-name: pim-flow-recede;
  animation-timing-function: ease-in-out;
  animation-fill-mode: both;
}

@keyframes pim-flow-recede {
  from { filter: blur(0) brightness(1); }
  to { filter: blur(2px) brightness(0.42); }
}

/* The pay-off: the headline scales up out of the hub. Sibling of the stage, not
   a child, so one element dims while a different element arrives. */
.pim-flow-payoff {
  position: absolute;
  left: 50%;
  /* Above centre, not on it: the buttons would otherwise land in the roster's
     top row, and the scrim alone cannot separate two things at the same height.
     Growing out of the hub still reads from here.

     The offset is expressed in the *stage's* units rather than as a percentage,
     and that is the whole point. The roster is pinned to a fraction of the stage
     height, and the stage height follows the stage width. A percentage `top`
     resolved against .pim-flow instead, whose height follows the viewport
     height — so the clearance between the buttons and the chips silently
     depended on the window's aspect ratio. It measured between 29px and 52px of
     *overlap* once the tagline grew a fourth line, and it had never been
     guaranteed at any size, only observed at the one that was checked.

     Now both ends are on the same ruler: sit on the stage's centre — 50% less
     the 1rem that .pim-flow's asymmetric padding puts between the two — then
     rise by a sixth of the stage height. The payoff is still centred on itself,
     so its own growth only eats half of that clearance, which is why the height
     below is kept lean as well.

     A sixth is enough that the block clears the roster at every size rather than
     only at wide ones: the stage shrinks with the viewport but a headline and
     four lines of tagline do not, so the narrow end of the desktop range is the
     tight one. It lifts the scrim's top edge a little above the stage there,
     which is invisible — the gradient has already faded out by its own edge, and
     .pim-flow keeps 3rem of its own padding above the stage. */
  top: 50%;
  --pim-flow-stage-h: calc(min(1160px, 100vw - 3rem) * 640 / 1160);
  /* A sixth of the stage, but never less than 95px. The stage shrinks with the
     viewport; a headline and four lines of tagline do not, so a proportional
     lift alone runs out just above the stacked breakpoint, where the stage is
     at its smallest and the block filling it is at full size. The floor is what
     that narrow end needs, and it stops applying by about 1080px, where a sixth
     is already the larger of the two — so the two regimes meet without a
     breakpoint to step across. */
  --pim-flow-payoff-lift: max(calc(var(--pim-flow-stage-h) / 6), 95px);
  transform: translate(-50%, calc(-50% - 1rem - var(--pim-flow-payoff-lift)));
  z-index: 2;
  width: min(760px, calc(100% - 2rem));
  padding: 1.8rem 1.5rem;
  text-align: center;
  /* A scrim, not a card: the app's login form brought its own opaque surface,
     and this has to sit over the settled roster without hiding it. Dark in the
     middle where the words are, gone by the edges where the chips still show. */
  background: radial-gradient(
    ellipse 68% 78% at 50% 48%,
    rgba(5, 14, 36, 0.88) 0%,
    rgba(5, 14, 36, 0.66) 52%,
    transparent 78%
  );
  animation-name: pim-flow-payoff-in;
  animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.25);
  animation-fill-mode: both;
}

@keyframes pim-flow-payoff-in {
  from { opacity: 0; scale: 0.18; }
  to { opacity: 1; scale: 1; }
}

/* Authored with a line break in the content file, honoured rather than turned
   into markup so a translation can break the line where its own words want to. */
.pim-flow-headline { white-space: pre-line; }

/* The hero's tagline is the one place the site-wide lede does not get to grow
   with the viewport. Everywhere else a wider window means a wider column, so a
   larger size keeps the same words-per-line; here the column is pinned to the
   scrim's 760px at every width, so letting the clamp run to its top just added
   a line inside a fixed box — the line that landed on the roster. It holds the
   clamp's floor instead, which is still a fifth larger than the size the review
   called too small, and a tighter leading than body copy because three or four
   centred lines under a headline are a block you take in, not a paragraph you
   read down. */
.pim-flow-payoff .pim-lede {
  margin-inline: auto;
  font-size: 1.12rem;
  line-height: 1.5;
  /* Same reason as .pim-flow-headline: a break authored in the content file,
     honoured rather than reproduced as markup, so a translation still controls
     its own line. */
  white-space: pre-line;
}

.pim-flow-payoff .pim-h1 { margin-bottom: 0.6rem; }

.pim-flow-payoff .pim-actions {
  justify-content: center;
  margin-top: var(--pim-space-3);
}


/* Only worth saying while there is something to skip: it fades out on the same
   cue the headline arrives on. */
.pim-flow-hint {
  position: absolute;
  left: 50%;
  bottom: 1.4rem;
  translate: -50% 0;
  z-index: 2;
  margin: 0;
  font-size: 0.76rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--pim-text-dim);
  opacity: 0;
  animation: pim-flow-hint 11s ease-in-out 1 both;
}

@keyframes pim-flow-hint {
  0%, 6% { opacity: 0; }
  12%, 72% { opacity: 0.6; }
  85%, 100% { opacity: 0; }
}

.pim-flow-scroll {
  position: absolute;
  left: 50%;
  bottom: 1.4rem;
  translate: -50% 0;
  z-index: 3;
  display: grid;
  justify-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.9rem;
  font-family: var(--pim-font-brand);
  font-size: 0.7rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--pim-text-dim);
  animation-name: pim-flow-scroll-in;
  animation-duration: 0.6s;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}

.pim-flow-scroll::after {
  content: "";
  width: 1px;
  height: 26px;
  background: linear-gradient(180deg, var(--pim-spark), transparent);
}

.pim-flow-scroll:hover { color: var(--pim-spark); }

@keyframes pim-flow-scroll-in {
  from { opacity: 0; translate: -50% -6px; }
  to { opacity: 1; translate: -50% 0; }
}

.pim-flow-wires {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* pathLength="100" normalises every curve, so one set of keyframes drives all
   of them at the same visual speed whatever their real length — for the draw-on
   and for the sparks alike. */
.pim-flow-line {
  fill: none;
  stroke: rgba(122, 176, 255, 0.26);
  stroke-width: 1.6;
  stroke-dasharray: 100 100;
  animation-name: pim-flow-draw;
  animation-timing-function: ease-in-out;
  animation-fill-mode: both;
}

@keyframes pim-flow-draw {
  from { stroke-dashoffset: 100; }
  to { stroke-dashoffset: 0; }
}

.pim-flow-spark {
  fill: none;
  stroke: #9beaff;
  stroke-width: 3.4;
  stroke-linecap: round;
  stroke-dasharray: 4 96;
  filter: drop-shadow(0 0 5px rgba(120, 220, 255, 0.9));
  /* Two animations: the endless travel, plus a one-shot reveal that keeps the
     dash off screen until its line has finished drawing. */
  animation-name: pim-flow-travel, pim-flow-spark-on;
  animation-timing-function: linear, linear;
  animation-iteration-count: infinite, 1;
  animation-fill-mode: none, both;
}

@keyframes pim-flow-travel {
  from { stroke-dashoffset: 0; }
  to { stroke-dashoffset: -100; }
}

@keyframes pim-flow-spark-on {
  from { opacity: 0; }
  to { opacity: 1; }
}

.pim-flow-hub {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Base is the mark's circle — 160/130 of the 7.6cqw tile — because the intro
     ends on the mark. The diagram-sized 13.5% lives in the keyframes below;
     the wire endpoints in index.html are cut to that radius (78.3 of the
     1160-unit stage, so the curves stop at 501.7 and 658.3). */
  width: var(--pim-logo-diameter, 9.354cqw);
  aspect-ratio: 1;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 50% 40%, #ffffff 0%, #eef6ff 58%, #cbe1ff 100%);
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.6),
    0 0 42px rgba(79, 195, 255, 0.45),
    0 0 120px rgba(46, 120, 255, 0.35);
  /* Arrives first, then flares each time a wave of badges lands in it. */
  animation-name: pim-flow-hub-in, pim-flow-absorb;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1), ease-out;
  animation-iteration-count: 1, 1;
  animation-fill-mode: both, none;
}

@keyframes pim-flow-hub-in {
  from { opacity: 0; scale: 0.72; }
  to { opacity: 1; scale: 1; }
}

@keyframes pim-flow-absorb {
  0%,
  30%,
  100% {
    box-shadow:
      0 0 0 1px rgba(255, 255, 255, 0.6),
      0 0 42px rgba(79, 195, 255, 0.45),
      0 0 120px rgba(46, 120, 255, 0.35);
  }
  7% {
    box-shadow:
      0 0 0 2px rgba(255, 255, 255, 0.9),
      0 0 78px rgba(155, 228, 255, 0.85),
      0 0 200px rgba(90, 170, 255, 0.6);
  }
}

/* Collapses toward the centre rather than expanding past the edge: outside the
   disc the ring had nothing to travel over and read as a second, static rim.
   Two rings half a cycle apart, so there is always one travelling — a single
   1px ring left the disc looking flat for most of the 3.6s. Ambient motion, so
   the delay belongs here and not with the choreography's inline styles. */
.pim-flow-hub::before,
.pim-flow-hub::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* Finished state: no ring. The mark's circle is a plain disc. */
  border: 2px solid transparent;
  animation-name: pim-flow-halo, pim-flow-halo-out;
  animation-duration: 3.6s, 0.5s;
  animation-timing-function: ease-out, ease-out;
  animation-iteration-count: infinite, 1;
  animation-fill-mode: none, both;
  animation-delay: 0s, 8.70s;
}

.pim-flow-hub::before { animation-delay: 1.8s, 8.70s; }

/* Not opacity: pim-flow-halo owns that, and a second animation on it would have
   its backwards fill hold one value across the whole run. */
@keyframes pim-flow-halo-out {
  from { border-color: rgba(255, 255, 255, 0.9); }
}

@keyframes pim-flow-logo-hub {
  from { width: 13.5%; }
}

@keyframes pim-flow-halo {
  0% { transform: scale(1); opacity: 0.95; }
  70% { transform: scale(0.52); opacity: 0; }
  100% { transform: scale(0.52); opacity: 0; }
}

/* Nodes and badges are positioned with `transform`, so everything that moves
   them uses the independent `scale` / `translate` properties instead — no
   fighting over one shorthand, and the same keyframes work in the stacked
   mobile layout where there is no centring transform at all. */
.pim-flow-node {
  position: absolute;
  transform: translate(-50%, -50%);
  translate: var(--pim-logo-x, 0) var(--pim-logo-y, 0);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7cqw;
  animation-name: pim-flow-node-in;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
  animation-fill-mode: both;
}

@keyframes pim-flow-node-in {
  from { opacity: 0; scale: 0.78; }
  to { opacity: 1; scale: 1; }
}

/* Both endpoints are exact at every stage width: the diagram position is a
   percentage of the stage, the mark's slot a cqw offset off its centre that
   scales with the tile. Only the path between them shifts with the viewport,
   which nothing depends on. `translate` is free here — node-in owns opacity and
   scale, and the -50% centring is on `transform`. */
@keyframes pim-flow-logo-place {
  from {
    left: var(--pim-from-left);
    top: var(--pim-from-top);
    translate: 0 0;
    gap: var(--pim-node-gap, 0.7cqw);
  }
}

/* The mark has no captions. */
.pim-flow-node .pim-flow-node-label {
  opacity: 0;
  line-height: 0;
  animation-name: pim-flow-label-out;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}

@keyframes pim-flow-label-out {
  from { opacity: 1; line-height: 1.25; }
}

.pim-flow-node-icon {
  width: 7.6cqw;
  height: 7.6cqw;
  border-radius: 1.6cqw;
  display: grid;
  place-items: center;
  color: #bfe3ff;
  background: linear-gradient(160deg, rgba(30, 62, 122, 0.9), rgba(11, 28, 66, 0.9));
  border: 1px solid rgba(127, 199, 255, 0.35);
  box-shadow:
    0 0 20px rgba(20, 60, 140, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
}

/* The mark's satellites are plain squares, so the glyph leaves when the mark
   forms. Base is gone; the keyframe holds it. */
.pim-flow-node-icon svg {
  width: 52%;
  height: 52%;
  opacity: 0;
  animation-name: pim-flow-tile-glyph-out;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}

@keyframes pim-flow-tile-glyph-out {
  from { opacity: 1; }
}

.pim-flow-node-label {
  font-family: var(--pim-font-brand);
  font-weight: 500;
  font-size: clamp(9px, 1.18cqw, 13.5px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #9dc4ee;
  white-space: nowrap;
}

/* lucide has no lettered file icon, so the PDF channel draws its own. Styled
   here rather than with SVG attributes so it inherits currentColor from the
   tile exactly like the real lucide glyphs beside it. */
.pim-flow-pdf {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.pim-flow-pdf text {
  fill: currentColor;
  stroke: none;
  text-anchor: middle;
  font-family: var(--pim-font-brand);
  font-weight: 700;
  font-size: 6.4px;
  letter-spacing: -0.15px;
}

/* —— Capability pills ——
   One look, two lives. `.pim-flow-pill` is the pastel chrome; `.pim-flow-badge`
   is the copy that falls in from above and is swallowed by the hub, and
   `.pim-flow-chip` is the copy that appears in the roster at that exact moment
   and stays for good. Only one of the pair is ever on screen. */
.pim-flow-pill {
  border-radius: 999px;
  text-align: center;
  line-height: 1.3;
  font-family: var(--pim-font-brand);
  font-weight: 600;
  font-size: clamp(8px, 1.02cqw, 12px);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--pim-flow-tone);
  text-shadow: 0 0 10px color-mix(in srgb, var(--pim-flow-tone) 45%, transparent);
  background: color-mix(in srgb, var(--pim-flow-tone) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--pim-flow-tone) 48%, transparent);
  box-shadow:
    0 0 20px color-mix(in srgb, var(--pim-flow-tone) 20%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--pim-flow-tone) 20%, transparent);
  backdrop-filter: blur(2px);
}

/* The same nine pastels site.css exposes as --pim-tone-*, under the name the
   diagram's own rules use. */
.pim-flow-pill--lilac { --pim-flow-tone: var(--pim-tone-lilac); }
.pim-flow-pill--mint { --pim-flow-tone: var(--pim-tone-mint); }
.pim-flow-pill--peach { --pim-flow-tone: var(--pim-tone-peach); }
.pim-flow-pill--sky { --pim-flow-tone: var(--pim-tone-sky); }
.pim-flow-pill--gold { --pim-flow-tone: var(--pim-tone-gold); }
.pim-flow-pill--rose { --pim-flow-tone: var(--pim-tone-rose); }
.pim-flow-pill--plum { --pim-flow-tone: var(--pim-tone-plum); }
.pim-flow-pill--aqua { --pim-flow-tone: var(--pim-tone-aqua); }
.pim-flow-pill--sage { --pim-flow-tone: var(--pim-tone-sage); }
.pim-flow-pill--coral { --pim-flow-tone: var(--pim-tone-coral, var(--pim-tone-peach)); }

.pim-flow-badge {
  --pim-flow-from-y: 0cqw;
  --pim-flow-hub-x: 0cqw;
  --pim-flow-hub-y: 0cqw;
  position: absolute;
  transform: translate(-50%, -50%);
  max-width: 19cqw;
  padding: 0.55cqw 1.15cqw;
  /* One pass only. What survives is the roster chip below the hub. */
  animation-name: pim-flow-consume;
  animation-iteration-count: 1;
  animation-fill-mode: both;
}

/* Enters from off the stage, settles, then falls into the well. The 78% stop is
   the moment of consumption, mirrored by BADGE_CONSUMED_AT in flow_diagram.py,
   which schedules both the hub's flare and the roster chip. Keep them in step.

   The crossing is a geometric ramp along one vector rather than a lean and a
   plunge: each leg is `linear`, and each covers more ground than the last, so
   the pill creeps, commits, then falls. An eased leg would decelerate into the
   next stop — the opposite of gravity. */
@keyframes pim-flow-consume {
  0% {
    opacity: 0;
    scale: 0.86;
    translate: 0 var(--pim-flow-from-y);
    animation-timing-function: cubic-bezier(0.3, 0, 0.5, 1);
  }
  10% {
    opacity: 1;
    scale: 0.94;
    translate: 0 calc(var(--pim-flow-from-y) * 0.45);
    animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
  }
  24% {
    opacity: 1;
    scale: 1;
    translate: 0 0;
    animation-timing-function: linear;
  }
  44% {
    opacity: 1;
    scale: 1;
    translate: calc(var(--pim-flow-hub-x) * 0.05) calc(var(--pim-flow-hub-y) * 0.05);
    animation-timing-function: linear;
  }
  58% {
    opacity: 1;
    scale: 1;
    translate: calc(var(--pim-flow-hub-x) * 0.15) calc(var(--pim-flow-hub-y) * 0.15);
    animation-timing-function: linear;
  }
  68% {
    opacity: 1;
    scale: 1;
    translate: calc(var(--pim-flow-hub-x) * 0.36) calc(var(--pim-flow-hub-y) * 0.36);
    animation-timing-function: linear;
  }
  74% {
    opacity: 1;
    scale: 0.62;
    translate: calc(var(--pim-flow-hub-x) * 0.62) calc(var(--pim-flow-hub-y) * 0.62);
    animation-timing-function: linear;
  }
  78% {
    opacity: 0;
    scale: 0.16;
    translate: var(--pim-flow-hub-x) var(--pim-flow-hub-y);
  }
  79%,
  100% {
    opacity: 0;
    scale: 0.16;
    translate: var(--pim-flow-hub-x) var(--pim-flow-hub-y);
  }
}

/* The roster: what the platform has at its disposal, three to a row. Width, gap
   and columns are set inline from flow_diagram.py, because the falling pills are
   placed on the same columns and must agree with this grid. */
.pim-flow-roster {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  align-items: stretch;
}

.pim-flow-chip {
  display: grid;
  place-content: center;
  padding: 0.5cqw 0.9cqw;
  animation-name: pim-flow-settle;
  animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.4);
  animation-fill-mode: both;
}

@keyframes pim-flow-settle {
  from { opacity: 0; scale: 0.55; }
  to { opacity: 1; scale: 1; }
}

/* Below the fold of a tablet the diagram stops being readable, so drop the
   wires and stack the nodes instead of shrinking everything into mush. With no
   wires there is no story left to tell either, so the choreography is off and
   every part is simply present. */
/* The exports ------------------------------------------------------------
   Each channel is one vertical quarter of the mark's gradient square, stacked
   in a column where the four output tiles used to sit, with its name beside it.
   When the data has arrived the four slide together into the whole square and
   the names hand over to one, so the right-hand end of the story is the mark's
   own right-hand square.

   Base styles are the merged state, as everywhere in this file: `animation:
   none` settles on one square labelled "Every Export", not on four loose bars.
   The split position lives in the keyframes, per element, as a cqw offset off
   the stage centre — the same ruler the badges travel on. */
.pim-flow-export {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 1.4cqw;
  translate: var(--pim-logo-x) -50%;
  animation-name: pim-flow-export-assemble;
  animation-timing-function: cubic-bezier(0.6, 0, 0.35, 1);
  animation-fill-mode: both;
}

/* Three states, one animation: two of them move `left` and `translate`, so a
   second animation on the same properties would have its backwards fill clobber
   the first from t=0. 6.66s + 3.04s: merged by 27.96%, held to 67.1% (8.70s),
   in the mark's right slot at 100%. */
@keyframes pim-flow-export-assemble {
  0% {
    left: var(--pim-from-left);
    translate: 0 calc(-50% + var(--pim-export-from-y));
  }
  27.96% {
    left: var(--pim-from-left);
    translate: var(--pim-export-x) -50%;
  }
  67.1% {
    left: var(--pim-from-left);
    translate: var(--pim-export-x) -50%;
  }
}

/* Holds the glyph and the name only. The bar is a sibling, outside this, so it
   is on screen from the first frame while these two wait for their wire to land
   — and the entrance can still own opacity and scale while the parent's merge
   owns translate, with no two animations on one property. */
.pim-flow-export-in {
  display: flex;
  align-items: center;
  gap: 1.4cqw;
  animation-name: pim-flow-node-in;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
  animation-fill-mode: both;
}

/* One gradient sized to the whole square, offset so slice i shows slice i's
   colours: side by side the four are continuous, and the merged square carries
   the mark's gradient end to end rather than four copies of it. */
.pim-flow-export-bar {
  display: block;
  /* 1px wider than its share, so merged strips overlap instead of abutting.
     Their edges land on fractional pixels — 1049.872, 1063.628, ... — and two
     antialiased edges meeting there leave a dark hairline even with the gap
     measured at 0.006px. The overlap duplicates 1px of a 55px gradient, which
     is under 2% of it and invisible. */
  width: calc(1.9cqw + 1px);
  height: 7.6cqw;
  border-radius: var(--pim-export-radius);
  background-image: linear-gradient(90deg, #2F7BFF, #22E3E9);
  /* 1px wider than the square, to match the strips. Each strip is its share
     plus 1px, but the gradient was sized to the square alone: strips 0-2 spend
     that extra pixel over their right neighbour and stay covered, while strip
     3 has no neighbour and its window ran out 1px before its box did. The last
     column was unpainted — and it is the column the right-hand corner radius
     is drawn on, so the rounding ended on transparent and the right edge lost
     a sliver. Sizing the gradient to the boxes paints all four to their edge;
     the extra pixel shifts each strip's colours by under 1px of a 21px slice. */
  background-size: calc(7.6cqw + 1px) 100%;
  background-position: calc(var(--pim-export-i) * -1.9cqw) 0;
  background-repeat: no-repeat;
  /* No glow once they are one square. An outer box-shadow paints above earlier
     siblings' backgrounds, so each strip's glow bled 18px over the strip to its
     left and the three seams showed as bright bands down the middle of the
     square. Apart, they are four lit bars and the glow belongs; the keyframe
     below holds that state. */
  box-shadow: none;
  animation-name: pim-flow-export-bar-merge;
  animation-timing-function: cubic-bezier(0.6, 0, 0.35, 1);
  animation-fill-mode: both;
}

/* Apart they are lines, so they round on every corner; together only the two
   outer slices keep a corner, which is what makes the seams disappear. */
@keyframes pim-flow-export-bar-merge {
  from {
    border-radius: 0.95cqw;
    box-shadow: 0 0 18px rgba(46, 160, 240, 0.45);
  }
  to {
    border-radius: var(--pim-export-radius);
    box-shadow: none;
  }
}

/* Rides along in its row, then travels to a 2x2 slot beside the merged square:
   the per-glyph offsets are the slot minus where the row itself lands, so the
   parent's merge and this gather compose instead of fighting. */
.pim-flow-export-glyph {
  opacity: 0;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 3.2cqw;
  height: 3.2cqw;
  color: #bfe3ff;
  translate: var(--pim-glyph-x) var(--pim-glyph-y);
  animation-name: pim-flow-export-glyph-gather, pim-flow-glyph-out;
  animation-timing-function: cubic-bezier(0.6, 0, 0.35, 1), ease-out;
  animation-fill-mode: both, both;
}

/* Overrides .pim-icon's fixed 1.5rem: these scale with the stage. */
.pim-flow-export-glyph svg {
  width: 100%;
  height: 100%;
}

@keyframes pim-flow-export-glyph-gather {
  from { translate: 0 0; }
  to { translate: var(--pim-glyph-x) var(--pim-glyph-y); }
}

@keyframes pim-flow-glyph-out {
  from { opacity: 1; }
}

.pim-flow-export-name {
  opacity: 0;
  animation-name: pim-flow-export-name-out;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}

@keyframes pim-flow-export-name-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* Wraps, and is capped in stage units: the label font bottoms out at 9px while
   the stage keeps shrinking, so a nowrap "Every Export" ran past the stage edge
   at the narrow end of the desktop range. Two lines at every width instead. */
.pim-flow-export-total {
  position: absolute;
  opacity: 0;
  top: 50%;
  translate: 0 -50%;
  max-width: 10cqw;
  white-space: normal;
  line-height: 1.25;
  animation-name: pim-flow-export-total-in;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
  animation-fill-mode: both;
}

/* In at 7.26s, out as the mark forms at 8.70s: one animation over 1.84s, so
   the base state is the mark's — nothing left of the label. */
@keyframes pim-flow-export-total-in {
  0% { opacity: 0; translate: -1.2cqw -50%; }
  27.2% { opacity: 1; translate: 0 -50%; }
  78.3% { opacity: 1; translate: 0 -50%; }
  100% { opacity: 0; translate: 0 -50%; }
}

/* Base state is visible, like everything else here; the keyframe holds the
   hidden start, so a skip lands on sparks running along a drawn trunk. */
/* Everything drawn in the SVG goes when the mark forms: the mark is five
   shapes, not a diagram. */
.pim-flow-outro {
  opacity: 0;
  animation-name: pim-flow-outro-off;
  animation-timing-function: ease-in;
  animation-fill-mode: both;
}

@keyframes pim-flow-outro-off {
  from { opacity: 1; }
}

/* The motto flanks the finished mark. Anchored off the stage centre in cqw, so
   the gap to the mark scales with it: the mark reaches 15.9cqw left (the
   diamond's vertex) and 13.4cqw right (the gradient square's edge). */
.pim-flow-motto {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  font-family: var(--pim-font-brand);
  font-weight: 600;
  /* Approaching the h1's own clamp(40px, 4vw, 50px). Wrapping is allowed at
     this size — "One catalog" needs about 27cqw at the top of the range and has
     30cqw of room, so it stays on one line until the type outgrows it. */
  font-size: clamp(20px, 3.6cqw, 44px);
  line-height: 1.1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #fff;
  max-width: 30cqw;
  text-shadow: 0 0 18px rgba(70, 150, 255, 0.45);
  animation-name: pim-flow-motto-in;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
  animation-fill-mode: both;
}

.pim-flow-motto--left {
  right: 50%;
  margin-right: 17.5cqw;
  text-align: right;
}

.pim-flow-motto--right {
  left: 50%;
  margin-left: 15cqw;
}

@keyframes pim-flow-motto-in {
  from { opacity: 0; scale: 0.92; }
}

/* The wordmark above the finished mark, and the tagline over the roster. */
.pim-flow-appname {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% calc(-50% - 19.6cqw);
  /* Manrope, not the brand face the rest of the diagram uses. */
  font-family: var(--pim-font-display);
  font-weight: 700;
  font-size: clamp(28px, 5.4cqw, 68px);
  letter-spacing: 0.02em;
  color: #fff;
  white-space: nowrap;
  text-shadow: 0 0 22px rgba(70, 150, 255, 0.5);
  animation-name: pim-flow-motto-in;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
  animation-fill-mode: both;
}

/* Hand kerning on the wordmark: air on both sides of the I, the hyphen pushed
   right off the M, and the T pulled back over it far enough to overlap. In em,
   so it holds at every size the clamp resolves to.

   These values are for Manrope and are about a third of what the same effect
   needed in Orbitron. Orbitron gives its flat-sided caps almost no side
   bearing, so the I sat wedged between the P and the M and wanted 0.10em to
   breathe; Manrope spaces it properly on its own and 0.10em over-opened it.
   The Orbitron set, if the face ever goes back:  0.10 / 0.22 / -0.26.

   The hyphen is spaced optically, and equal ink gaps are the wrong target for
   it. Measured at the real 49px: the hyphen-to-T gap sits at 4px whatever the T
   is given, because Manrope's own hyphen+T kern pair sets it and any non-zero
   margin on the T only breaks that pair and widens it. So the right side is
   fixed and the left is the only lever.

   Equal ink gaps — 5px and 5px, which -0.03em / 0.03em produced — read as a
   small gap on the left and a large one on the right, because the hyphen sits
   at mid-height while the T's left arm has an empty counter under it all the
   way to the baseline. Balancing that needs *more* ink on the left, not the
   same: 0.04em gives 8px against the T's 4px, and reads centred.

   Also worth knowing if these are re-derived: a margin on an inline span shifts
   everything after it, so moving the hyphen does not change the gap on its
   right at all. The T stays at 0 to keep the font's kern.

     0.02 / 0    left 7px    slightly left-leaning
     0.04 / 0    left 8px    reads centred               <- in use
     0.06 / 0    left 10px   starts to drift right */
.pim-flow-appname-i {
  position: relative;
  display: inline-block;
  line-height: 1;
  margin-inline: 0.07em;
}

.pim-flow-appname-dash { margin-left: 0.04em; }
.pim-flow-appname-t { margin-left: 0; }

/* Anchored to the bottom and grown upward, so it lands over the roster whatever
   its own height turns out to be — the two paragraphs take three lines wide and
   four narrow. The scrim is what lets it sit on top of the chips: they stay
   faintly visible through it rather than being covered or removed. */
.pim-flow-outro-copy {
  position: absolute;
  left: 50%;
  /* Centred on the roster's own 84.5%, a shade lower, so it covers the chips
     rather than half of them and still clears the mark's lower square at 73%
     where the stage is at its shortest. */
  top: 87%;
  translate: -50% -50%;
  width: 98cqw;
  /* Tight, because the block has to fit between the mark's lower square at
     73.4% and the stage's own bottom — about 24% of the stage's height, and the
     larger type wants 28% of it. The spacing gives that back rather than the
     font size doing it. */
  padding: 1.0cqw 2.2cqw;
  /* A scrim, not a box, and on a pseudo-element rather than this one. A radial
     gradient *background* faded from the centre, which meant the ends of the
     long first line sat on almost nothing. This is a flat fill with a soft mask
     instead: full opacity under every line, falling off on all four sides, so
     there is still no visible edge.

     The mask has to be on ::before, not here. A mask applies to the element and
     its content alike, so masking this box would fade the text at the edges
     along with the fill. `isolation: isolate` keeps the negative z-index inside
     this element rather than dropping it behind the page.

     No backdrop-filter either: the blur applies to the whole element box and
     would put back the hard rectangle the fade exists to remove. */
  isolation: isolate;
  text-align: center;
  animation-name: pim-flow-motto-in;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
  animation-fill-mode: both;
}

.pim-flow-outro-copy::before {
  content: "";
  position: absolute;
  /* Slightly beyond the text, so the plateau covers it and the fade happens
     outside it. */
  inset: -0.5cqw -1.2cqw;
  z-index: -1;
  background: rgba(5, 14, 36, 0.86);
  -webkit-mask-image: radial-gradient(ellipse 82% 94% at 50% 50%, #000 0%, #000 62%, transparent 100%);
  mask-image: radial-gradient(ellipse 82% 94% at 50% 50%, #000 0%, #000 62%, transparent 100%);
}

.pim-flow-outro-copy p {
  margin: 0;
  font-size: clamp(9px, 1.6cqw, 18px);
  line-height: 1.55;
  color: #c9dbec;
}

.pim-flow-outro-copy p + p { margin-top: 0.45em; }

.pim-flow-outro-actions {
  display: flex;
  justify-content: center;
  gap: 0.9cqw;
  margin-top: 0.85cqw;
}

/* The stage is a container-query context, so the buttons can scale with the
   diagram instead of keeping the site's fixed 38px height inside it. */
.pim-flow-outro-actions .pim-btn {
  min-height: 0;
  padding: 0.75cqw 1.6cqw;
  font-size: clamp(9px, 1.25cqw, 14px);
}

.pim-flow-trunk-sparks {
  animation-name: pim-flow-trunk-sparks-in;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}

@keyframes pim-flow-trunk-sparks-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The fan reached four separate bars; once they are one square it has nothing
   to fan to, so it retires and the trunk below carries the flow instead. */
.pim-flow-fan-out {
  opacity: 0;
  animation-name: pim-flow-fan-out-off;
  animation-timing-function: ease-in;
  animation-fill-mode: both;
}

@keyframes pim-flow-fan-out-off {
  from { opacity: 1; }
  to { opacity: 0; }
}

.pim-flow-trunk { stroke: rgba(122, 176, 255, 0.4); }

@media (max-width: 820px) {
  .pim-flow {
    flex-direction: column;
    min-height: 0;
    padding: 2.5rem 1rem 3.5rem;
  }

  .pim-flow-stage {
    aspect-ratio: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.1rem;
    /* Nothing sits on top of the diagram here, so there is nothing to dim
       behind — and the base style is the dimmed one. */
    filter: none;
    animation: none;
  }

  .pim-flow-payoff {
    position: static;
    transform: none;
    animation: none;
    width: 100%;
    margin-top: 2.5rem;
  }

  /* Both are cues for a sequence that does not play here. */
  .pim-flow-hint,
  .pim-flow-scroll { display: none; }

  .pim-flow-wires { display: none; }

  /* Stays positioned: the ::after halo is absolute and would otherwise resolve
     against .pim-flow and stretch across the whole section. */
  .pim-flow-hub {
    position: relative;
    left: auto;
    top: auto;
    transform: none;
    width: 160px;
    order: 2;
    margin: 0.5rem 0;
    animation: none;
  }

  .pim-flow-node {
    position: static;
    transform: none;
    flex-direction: row;
    justify-content: flex-start;
    gap: 0.9rem;
    width: 210px;
    animation: none;
  }

  .pim-flow-node--in { order: 1; }

  .pim-flow-badge { display: none; }

  .pim-flow-roster {
    order: 3;
    position: static;
    transform: none;
    width: auto !important;
    grid-template-columns: 1fr !important;
    justify-items: center;
  }

  .pim-flow-chip {
    animation: none;
    padding: 5px 12px;
    font-size: 10.5px;
  }

  .pim-flow-node-icon {
    width: 46px;
    height: 46px;
    border-radius: 12px;
  }

  .pim-flow-node-label { font-size: 13px; }
}

/* Click or keypress skips the story. Everything one-shot drops to its finished
   base style; the ambient motion keeps running, which is the difference between
   skipping to the end and freezing (the reduced-motion block below). Their
   delays are inline styles from flow_diagram.py, so only `!important` can zero
   them. */
.pim-flow--skipped .pim-flow-stage,
.pim-flow--skipped .pim-flow-hub,
.pim-flow--skipped .pim-flow-line,
.pim-flow--skipped .pim-flow-node,
.pim-flow--skipped .pim-flow-export,
.pim-flow--skipped .pim-flow-export-in,
.pim-flow--skipped .pim-flow-export-bar,
.pim-flow--skipped .pim-flow-export-glyph,
.pim-flow--skipped .pim-flow-export-name,
.pim-flow--skipped .pim-flow-export-total,
.pim-flow--skipped .pim-flow-fan-out,
.pim-flow--skipped .pim-flow-trunk-sparks,
.pim-flow--skipped .pim-flow-outro,
.pim-flow--skipped .pim-flow-motto,
.pim-flow--skipped .pim-flow-appname,
.pim-flow--skipped .pim-flow-outro-copy,
.pim-flow--skipped .pim-flow-node-label,
.pim-flow--skipped .pim-flow-node-icon svg,
.pim-flow--skipped .pim-flow-chip,
.pim-flow--skipped .pim-flow-payoff,
.pim-flow--skipped .pim-flow-scroll,
.pim-flow--skipped .pim-flow-hint {
  animation: none;
}

.pim-flow--skipped .pim-flow-badge { display: none; }
.pim-flow--skipped .pim-flow-hint { opacity: 0; }

/* The ambient pulse is on pseudo-elements, so the list above cannot switch it
   off — and a skip has to land on the mark's plain circle, not a pulsing one. */
.pim-flow--skipped .pim-flow-hub::before,
.pim-flow--skipped .pim-flow-hub::after {
  animation: none;
}

.pim-flow--skipped .pim-flow-spark {
  animation-delay: 0s, 0s !important;
}

@media (prefers-reduced-motion: reduce) {
  .pim-flow-spark,
  .pim-flow-hub::before,
  .pim-flow-hub::after,
  .pim-flow-hint,
  /* Its roster chip is already the permanent record; showing the falling copy
     with no motion left would just be the same label twice. */
  .pim-flow-badge {
    display: none;
  }

  /* Everything settles into its finished state immediately — see the note at
     the top about why the hidden states live in the keyframes. */
  .pim-flow-stage,
  .pim-flow-hub,
  .pim-flow-line,
  .pim-flow-node,
  .pim-flow-export,
  .pim-flow-export-in,
  .pim-flow-export-bar,
  .pim-flow-export-glyph,
  .pim-flow-export-name,
  .pim-flow-export-total,
  .pim-flow-fan-out,
  .pim-flow-trunk-sparks,
  .pim-flow-outro,
  .pim-flow-motto,
  .pim-flow-appname,
  .pim-flow-outro-copy,
  .pim-flow-node-label,
  .pim-flow-node-icon svg,
  .pim-flow-chip,
  .pim-flow-payoff,
  .pim-flow-scroll {
    animation: none;
  }
}
