/* ==========================================================================
   PULSAR DEMO SYSTEM  ·  v2  ·  namespace: .pd-*
   ==========================================================================
   A self-contained design system for the animated product demos on the
   Solutions pages. Separate from main.css / solution-page.css on purpose:
   product-surface styling and marketing-copy styling must not be able to bleed
   into one another.

   WHAT THIS IS AIMING AT
   ----------------------
   A visitor should believe they are looking at a screen recording of PULSAR.
   Two things carry that, and neither is motion:

     1. THE SHELL. Real applications have a sidebar, a top bar, breadcrumbs,
        tabs, a status line, and dense small type. A drawing floating on a dark
        panel reads as an illustration no matter how well it animates. The
        .pd-app__* components below are that shell, and every demo wears it.
     2. RESTRAINT. Real products are not neon. Most of the surface is quiet
        greys; teal appears only where data actually is; orange appears once.

   NOTHING IN A DEMO IS INTERACTIVE. No control, no hover state, no focusable
   element. The visitor watches; they do not drive. `pointer-events: none` is
   set at the root and must stay there.

   HOW TO ADJUST THINGS LATER
   --------------------------
   · Colour, spacing, motion speed      -> the token block below, nothing else.
   · The application shell              -> § APP SHELL.
   · A primitive every demo can reuse   -> § PRIMITIVES.
   · What ONE demo's beats look like    -> css/demo/<flow>.css
   · The beats, durations, the pointer  -> js/demo/flows/<flow>.js
   · Timeline, typing, pointer mechanics-> js/demo/engine.js

   THE ONE RULE: the engine only ever writes data-pd-step (and data-pd-index)
   onto the demo root. Every visual consequence is expressed in CSS.

   COLOUR IS FUNCTIONAL (inherited from the AIORX brand system):
     teal   -> data, connections, intelligence
     orange -> action, and the single flagged deviation
     red    -> failure only. Never decoration.
   ========================================================================== */

.pd {
  /* —— Surfaces. A real app has more than one grey; these are the strata. —— */
  --pd-edge: #0c1524;        /* chrome: sidebar, top bar, status bar */
  --pd-body: #101c30;        /* the working canvas */
  --pd-raised: #17253c;      /* cards, rows, panels sitting on the canvas */
  --pd-sunken: #0a1120;      /* wells: code, logs, inputs */

  /* —— Lines —— */
  --pd-line: rgba(141, 153, 174, 0.16);
  --pd-line-strong: rgba(141, 153, 174, 0.32);

  /* —— Text —— */
  --pd-text: #e8e9ed;
  --pd-text-soft: #aab4c5;
  --pd-text-dim: #74819a;

  /* —— Functional accents —— */
  --pd-data: #4ecdc4;
  --pd-data-soft: rgba(78, 205, 196, 0.14);
  --pd-data-glow: rgba(78, 205, 196, 0.4);
  --pd-action: #ef8354;
  --pd-action-soft: rgba(239, 131, 84, 0.14);
  --pd-fail: #d64550;

  /* —— LIVE. The one colour reserved for things a visitor can actually press.
     Every other hue on these surfaces already carries a meaning inside the
     story being told — teal is data, orange is action, red is failure — so
     none of them can also mean "this one is real" without becoming ambiguous
     the moment they appear next to each other. Violet appears nowhere in the
     product story, which is exactly why it can carry this: if it is violet,
     you can press it, and nothing you can press is any other colour. —— */
  --pd-live: #a78bfa;
  --pd-live-soft: rgba(167, 139, 250, 0.16);
  --pd-live-line: rgba(167, 139, 250, 0.55);
  /* The same violet, darkened for text on the light page ground. #a78bfa on
     white is around 2.3:1 and unreadable at caption size; this clears 4.5:1. */
  --pd-live-ink: #5b4bb8;

  /* —— Geometry. Tighter than marketing UI: products are dense. —— */
  --pd-radius: 10px;
  --pd-radius-sm: 4px;
  --pd-gap: 12px;
  --pd-pad: 14px;
  --pd-side-w: 168px;

  /* —— Type. Small, because real tools are small. —— */
  --pd-font: var(--font-sans, "DM Sans", system-ui, sans-serif);
  --pd-mono: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace);
  --pd-size-label: 0.62rem;   /* uppercase mono micro-label */
  --pd-size-meta: 0.7rem;     /* run ids, timestamps, paths */
  --pd-size-body: 0.78rem;    /* readable copy inside the surface */
  --pd-size-title: 0.9rem;

  /* —— Motion —— */
  --pd-t-fast: 160ms;
  --pd-t: 380ms;
  --pd-t-slow: 720ms;
  --pd-ease: cubic-bezier(0.22, 0.8, 0.28, 1);

  display: block;
}

/* --------------------------------------------------------------------------
   THE BACKDROP
   The app panel floats inset on a branded gradient card rather than sitting
   flat on the page. This is doing more work than it looks: an inset panel
   reads as a product being presented, where a full-bleed panel reads as a
   diagram drawn into the page. It also gives the dark surface an edge on a
   light band without needing a border to say so.
   -------------------------------------------------------------------------- */

.pd__frame {
  position: relative;
  padding: clamp(14px, 2.4vw, 34px);
  overflow: hidden;
  border-radius: calc(var(--pd-radius) + 8px);
  background:
    radial-gradient(120% 140% at 8% 0%, rgba(78, 205, 196, 0.5) 0%, transparent 55%),
    radial-gradient(100% 120% at 100% 100%, rgba(91, 159, 212, 0.45) 0%, transparent 60%),
    linear-gradient(150deg, #14304a 0%, #0e2036 55%, #101b2e 100%);
}

/* A faint engineering grid over the gradient, masked out towards the edges so
   it never competes with the panel it sits behind. */
.pd__frame::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.07) 1px, transparent 1px);
  background-size: 26px 26px;
  mask-image: radial-gradient(ellipse at 30% 10%, #000 20%, transparent 75%);
}

/* --------------------------------------------------------------------------
   THE APP FRAME
   -------------------------------------------------------------------------- */

.pd__app {
  position: relative;
  display: grid;
  grid-template-columns: var(--pd-side-w) minmax(0, 1fr);
  overflow: hidden;
  border: 1px solid var(--pd-line-strong);
  border-radius: var(--pd-radius);
  background: var(--pd-body);
  box-shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.75), inset 0 1px 0 rgba(255, 255, 255, 0.04);
  color: var(--pd-text);
  font-family: var(--pd-font);
  /* Not a toy: nothing here responds to a cursor. */
  pointer-events: none;
  user-select: none;
  contain: paint;
}

/* THE ONE EXCEPTION.
   A flow declaring `interactive: true` gets its surface back: the engine skips
   the aria-hidden it otherwise sets, adds this class, and the panel becomes
   real operable content rather than a picture.

   Both halves are required and they are not separable. Exposing the surface to
   assistive tech while it stays `pointer-events: none` gives a screen-reader
   user controls that cannot be activated; taking pointer events back while the
   surface stays aria-hidden gives everyone else controls that assistive tech
   cannot see. The "focusable but hidden" state is worse than either.

   If you use this, every control inside must be a real <button>, keyboard
   operable, with visible focus. */
.pd--interactive .pd__app {
  pointer-events: auto;
  user-select: auto;
}

/* An interactive demo has no invisible driver, so the pointer would be a lie. */
.pd--interactive .pd__pointer { display: none; }

/* THE OTHER EXCEPTION, and a much narrower one: the tab strip.
   The strip names the beats, so it is the obvious thing to press — and pressing
   it did nothing, which is worse than not looking pressable at all. The engine
   upgrades each tab to a real <button> and keeps the strip out of the app's
   aria-hidden subtree, so this is the pointer-events half of the same deal. The
   rest of the surface stays inert. */
.pd--tabbed .pd-app__tabs { pointer-events: auto; user-select: auto; }

/* The transport sits in the status bar, inside the frame — so it needs its
   pointer events back too. Only the buttons, not the row around them. */
.pd .pd__controls { pointer-events: auto; user-select: auto; }

/* Height comes from a modifier so no flow inlines a pixel value. */
.pd--h-sm .pd__app { min-height: 300px; }
.pd--h-md .pd__app { min-height: 400px; }
.pd--h-lg .pd__app { min-height: 480px; }

/* ==========================================================================
   § APP SHELL
   ========================================================================== */

/* — Sidebar — */
.pd-app__side {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--pd-pad) 10px;
  border-right: 1px solid var(--pd-line);
  background: var(--pd-edge);
}

.pd-app__brand {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: var(--pd-pad);
  padding: 0 6px;
  font-family: var(--pd-mono);
  font-size: var(--pd-size-meta);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.pd-app__brand::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--pd-data);
  box-shadow: 0 0 0 3px var(--pd-data-soft);
}

.pd-nav__label {
  margin: var(--pd-gap) 0 4px;
  padding: 0 6px;
  font-family: var(--pd-mono);
  font-size: 0.56rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--pd-text-dim);
}

.pd-nav__item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 7px;
  border-radius: var(--pd-radius-sm);
  font-size: var(--pd-size-body);
  color: var(--pd-text-soft);
  white-space: nowrap;
}

/* A leading glyph slot. Real navs have icons; a bare list of words does not
   read as software. Kept as a small geometric mark, not an icon set. */
.pd-nav__item::before {
  content: "";
  flex: none;
  width: 11px;
  height: 11px;
  border: 1.25px solid currentColor;
  border-radius: 3px;
  opacity: 0.5;
}

.pd-nav__item--current {
  background: var(--pd-raised);
  color: var(--pd-text);
  box-shadow: inset 2px 0 0 var(--pd-data);
}

.pd-nav__item--current::before {
  border-color: var(--pd-data);
  background: var(--pd-data-soft);
  opacity: 1;
}

/* — Main column — */
.pd-app__main {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* — Top bar: breadcrumbs left, run context right — */
.pd-app__topbar {
  display: flex;
  align-items: center;
  gap: var(--pd-gap);
  flex: none;
  padding: 9px var(--pd-pad);
  border-bottom: 1px solid var(--pd-line);
  background: var(--pd-edge);
  /* Wraps, because a title plus two context chips clips at phone width and the
     chips are usually the run and the timestamp — the two things a visitor most
     needs in order to believe the screen. Found on one page and predicted on the
     other two, which is exactly the case for fixing it here. Row-gap stays 0 so
     a wrapped chip does not open a gap in the chrome. */
  flex-wrap: wrap;
  row-gap: 0;
}

.pd-app__title {
  font-size: var(--pd-size-title);
  font-weight: 600;
  white-space: nowrap;
}

.pd-app__spacer { margin-left: auto; }

/* — Tab strip — */
.pd-app__tabs {
  display: flex;
  align-items: flex-end;
  gap: 1px;
  flex: none;
  padding: 0 var(--pd-pad);
  border-bottom: 1px solid var(--pd-line);
  background: var(--pd-edge);
  /* Five tabs overflow the panel below ~420px. Scroll rather than wrap: a
     wrapping strip changes the chrome's height, and the whole illusion rests on
     the surface being the same height on every beat. Real tool tab strips scroll
     too, and a half-visible tab at the edge reads as correct rather than broken.
     Both narrow-width flows hit this independently before it moved here. */
  overflow-x: auto;
  scrollbar-width: none;
}

.pd-app__tabs::-webkit-scrollbar { display: none; }

/* A tab per beat. The engine marks the current one as the timeline reaches it,
   so the strip is the flow's progress indicator and its table of contents at
   once — and unlike a step rail, it is plainly a readout, not a control.
   Uppercase mono at small size, because that is how tool chrome is set. */
/* ONE SHAPE MEANS ONE THING: a pill is pressable, a square corner is a picture.
   The fake controls on these surfaces — .pd-btn and friends — are outlined,
   filled and square-cornered, so an outline alone cannot separate the live from
   the painted. The full 999px radius can: nothing drawn inside the app frame
   uses it, so it is free to mean "this one is real". Tabs therefore stop being
   an underlined strip and become a segmented control once the engine has
   upgraded them to buttons. Before that they are plain spans and correctly look
   like labels. */
.pd--tabbed .pd-tab {
  cursor: pointer;
  appearance: none;
  border: 1px solid var(--pd-live-line);
  border-radius: 999px;
  background: var(--pd-live-soft);
  color: var(--pd-live);
  padding: 5px 11px;
  transition: color var(--pd-t) var(--pd-ease),
    border-color var(--pd-t) var(--pd-ease),
    background var(--pd-t) var(--pd-ease);
}

.pd--tabbed .pd-tab:hover {
  color: #c7b6ff;
  border-color: var(--pd-live);
  background: rgba(167, 139, 250, 0.26);
}

.pd--tabbed .pd-tab:hover::before { opacity: 0.85; }

.pd--tabbed .pd-tab:active { transform: translateY(1px); }

/* The step it is on: the same violet, filled. It is still a button — the
   marking says "you are here", not "this one is different in kind". */
.pd--tabbed .pd-tab.pd-tab--current {
  border-color: var(--pd-live);
  background: rgba(167, 139, 250, 0.32);
  color: #ffffff;
}

.pd--tabbed .pd-tab::before { background: currentColor; }

.pd--tabbed .pd-tab:focus-visible {
  outline: 2px solid var(--pd-live);
  outline-offset: 2px;
}

/* Nothing else on the surface may hint at being pressable. */
.pd__app { cursor: default; }
.pd--tabbed .pd-app__tabs { gap: 6px; }

.pd-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  border-bottom: 1px solid transparent;
  font-family: var(--pd-mono);
  font-size: var(--pd-size-label);
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--pd-text-dim);
  white-space: nowrap;
  transition: color var(--pd-t) var(--pd-ease), border-color var(--pd-t) var(--pd-ease);
}

/* The dot marker: present but unlit on every tab, so marking the current one
   changes its colour rather than adding an element and shifting the strip. */
.pd-tab::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.4;
  transition: background var(--pd-t) var(--pd-ease), opacity var(--pd-t) var(--pd-ease);
}

.pd-tab--current {
  border-bottom-color: var(--pd-data);
  color: var(--pd-text);
}

.pd-tab--current::before {
  background: var(--pd-data);
  opacity: 1;
  box-shadow: 0 0 0 3px var(--pd-data-soft);
}

/* — Working canvas — */
.pd-app__body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--pd-pad);
}

/* — Status bar: the detail that most says "application" — */
.pd-app__status {
  display: flex;
  align-items: center;
  gap: var(--pd-gap);
  flex: none;
  padding: 6px var(--pd-pad);
  border-top: 1px solid var(--pd-line);
  background: var(--pd-edge);
  font-family: var(--pd-mono);
  font-size: 0.62rem;
  letter-spacing: 0.04em;
  color: var(--pd-text-dim);
}

.pd-app__status-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--pd-data);
}

/* ==========================================================================
   § THE INVISIBLE USER
   The pointer and the typing caret. These two are what turn a screenshot into
   a recording — more than any transition does.
   ========================================================================== */

.pd__pointer {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 20;
  width: 15px;
  height: 15px;
  margin: -2px 0 0 -2px;
  opacity: 0;
  pointer-events: none;
  transition: transform 620ms var(--pd-ease), opacity var(--pd-t) var(--pd-ease);
  /* A real macOS-ish arrow, drawn rather than imported. */
  background: no-repeat center/contain
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 2l0 17 4.2-4.2 2.6 6.2 3.1-1.3-2.6-6.1 5.9 0z" fill="%23ffffff" stroke="%230c1524" stroke-width="1.4" stroke-linejoin="round"/></svg>');
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.5));
}

.pd__pointer.is-on { opacity: 1; }

/* The click: a ring that expands and fades from where the pointer is. */
.pd__pointer.is-clicking::after {
  content: "";
  position: absolute;
  top: 1px;
  left: 1px;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border: 1.5px solid var(--pd-data);
  border-radius: 50%;
  animation: pd-click 400ms var(--pd-ease) forwards;
}

@keyframes pd-click {
  from { transform: scale(0.2); opacity: 0.9; }
  to { transform: scale(1.6); opacity: 0; }
}

/* What the pointer presses, presses back. Without this the click lands on
   nothing and the gesture reads as decorative. */
.is-pressed {
  filter: brightness(1.25);
  transition: filter var(--pd-t-fast) var(--pd-ease);
}

/* The typing caret, blinking beside whatever text is being typed. */
.pd-caret {
  display: inline-block;
  width: 1px;
  height: 1.05em;
  margin-left: 1px;
  background: var(--pd-data);
  vertical-align: -2px;
  animation: pd-blink 1.05s steps(1) infinite;
}

@keyframes pd-blink {
  50% { opacity: 0; }
}

/* ==========================================================================
   § NARRATION AND PROGRESS  ·  both OUTSIDE the app frame
   A caption bar inside the surface is a tell: no real product has one. Out
   here it reads as the site talking about the demo, which is what it is.
   ========================================================================== */

/* THE META LAYER SPEAKS IN VIOLET.
   Two dimensions, not one. HUE says which layer you are looking at: violet is
   the site talking about the demo — the narration, the legend, the synthetic
   note, the controls. Everything inside the frame is the product picture and
   keeps the product's own colours. CONTAINER says what you can do: a violet
   PILL is pressable, violet TEXT is not. So the whole meta layer reads as one
   thing without violet losing its grip on "you can press this". */
.pd__narration {
  min-height: 3em;
  margin: var(--pd-gap) 0 0;
  font-family: var(--pd-font);
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--pd-live-ink, #5b4bb8);
}

.pd__narration strong { color: #3d2f8f; font-weight: 600; }

.band--dark .pd__narration { color: #c3b5fb; }
.band--dark .pd__narration strong { color: #e6dfff; }

.pd__progress {
  height: 2px;
  margin-top: 6px;
  overflow: hidden;
  border-radius: 2px;
  background: rgba(141, 153, 174, 0.2);
  pointer-events: none;
}

.pd__progress-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--pd-data);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--pd-t) linear;
}

/* ==========================================================================
   § PRIMITIVES  ·  the shared vocabulary every flow builds from
   A flow needing something new adds it HERE so the next flow inherits it.
   ========================================================================== */

.pd-label {
  display: block;
  font-family: var(--pd-mono);
  font-size: var(--pd-size-label);
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--pd-text-dim);
}

.pd-panel {
  padding: 11px;
  border: 1px solid var(--pd-line);
  border-radius: var(--pd-radius-sm);
  background: var(--pd-raised);
}

.pd-panel--well { background: var(--pd-sunken); }

.pd-panel--data {
  border-color: rgba(78, 205, 196, 0.35);
  background: var(--pd-data-soft);
}

/* Panel header: a title bar with a label, as every real panel has. */
.pd-panel__head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: -11px -11px 10px;
  padding: 7px 11px;
  border-bottom: 1px solid var(--pd-line);
  background: rgba(0, 0, 0, 0.18);
  border-radius: var(--pd-radius-sm) var(--pd-radius-sm) 0 0;
}

.pd-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border: 1px solid var(--pd-line-strong);
  border-radius: 999px;
  background: var(--pd-raised);
  font-family: var(--pd-mono);
  font-size: var(--pd-size-label);
  letter-spacing: 0.05em;
  color: var(--pd-text-soft);
  white-space: nowrap;
}

.pd-chip--data { border-color: rgba(78, 205, 196, 0.4); color: var(--pd-data); }
.pd-chip--action { border-color: rgba(239, 131, 84, 0.4); color: var(--pd-action); }

/* The text field the invisible user types into. */
.pd-field {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 12px;
  border: 1px solid var(--pd-line-strong);
  border-radius: var(--pd-radius-sm);
  background: var(--pd-sunken);
  font-size: var(--pd-size-body);
  color: var(--pd-text);
}

.pd-field.is-on { border-color: rgba(78, 205, 196, 0.5); }

/* Its text must not reflow the field as it types: fixed line, clipped. */
.pd-field__text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

/* A button drawn inside the product surface. Looks pressable, is not. */
.pd-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--pd-line-strong);
  border-radius: var(--pd-radius-sm);
  background: var(--pd-raised);
  font-family: var(--pd-font);
  font-size: var(--pd-size-label);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--pd-text-soft);
}

.pd-btn--action {
  border-color: rgba(239, 131, 84, 0.5);
  background: var(--pd-action-soft);
  color: var(--pd-action);
}

/* A selectable row: run lists, result lists, ticket lists. */
.pd-row {
  display: flex;
  align-items: center;
  gap: var(--pd-gap);
  padding: 7px 9px;
  border: 1px solid transparent;
  border-radius: var(--pd-radius-sm);
  font-size: var(--pd-size-body);
  color: var(--pd-text-soft);
}

.pd-row + .pd-row { margin-top: 2px; }

.pd-row.is-on {
  border-color: rgba(78, 205, 196, 0.4);
  background: var(--pd-data-soft);
  color: var(--pd-text);
}

.pd-row__id { font-family: var(--pd-mono); color: var(--pd-text); }
.pd-row__meta { margin-left: auto; font-family: var(--pd-mono); font-size: var(--pd-size-meta); }

/* Monospace source excerpt. Synthetic data only, always. */
.pd-log {
  margin: 0;
  overflow-x: auto;
  font-family: var(--pd-mono);
  font-size: var(--pd-size-meta);
  line-height: 1.7;
  color: var(--pd-text-soft);
  white-space: pre;
}

.pd-log mark {
  padding: 1px 4px;
  border-radius: 3px;
  background: var(--pd-action-soft);
  color: var(--pd-text);
}

.pd-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-family: var(--pd-mono);
  font-size: var(--pd-size-meta);
  color: var(--pd-text-dim);
}

.pd-crumbs__sep { color: var(--pd-line-strong); }
.pd-crumbs__item[data-pd-current="true"] { color: var(--pd-data); }

/* — Graph primitives, for use inside an <svg> on the canvas — */
.pd-edge {
  fill: none;
  stroke: var(--pd-line-strong);
  stroke-width: 1.25;
  transition: stroke var(--pd-t) var(--pd-ease), stroke-width var(--pd-t) var(--pd-ease),
    opacity var(--pd-t) var(--pd-ease);
}

.pd-edge.is-on { stroke: var(--pd-data); stroke-width: 1.75; }
.pd-edge.is-flagged { stroke: var(--pd-action); stroke-width: 2; }

.pd-node {
  fill: var(--pd-raised);
  stroke: var(--pd-line-strong);
  stroke-width: 1.25;
  transition: fill var(--pd-t) var(--pd-ease), stroke var(--pd-t) var(--pd-ease),
    opacity var(--pd-t) var(--pd-ease);
}

.pd-node.is-on { fill: var(--pd-data-soft); stroke: var(--pd-data); }
.pd-node.is-flagged { fill: var(--pd-action-soft); stroke: var(--pd-action); }
.pd-node.is-origin { fill: rgba(214, 69, 80, 0.2); stroke: var(--pd-fail); }

.pd-node-label {
  fill: var(--pd-text-soft);
  font-family: var(--pd-font);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  /* Halo, so a traced edge passing behind a label never cuts a glyph.
     paint-order puts the stroke under the fill — the only way to outline SVG
     text without thickening the letterforms. */
  paint-order: stroke;
  stroke: var(--pd-body);
  stroke-width: 3px;
  stroke-linejoin: round;
  transition: fill var(--pd-t) var(--pd-ease), opacity var(--pd-t) var(--pd-ease);
}

.pd-node-label.is-on { fill: var(--pd-text); }

/* ==========================================================================
   § DENSITY  ·  the vocabulary that makes a surface read as a real tool
   ==========================================================================
   A product screen is dense. Every region carries something actionable, and the
   volume of it is what tells a visitor the system is handling real complexity.
   A demo built from three big shapes reads as a diagram no matter how well it
   animates; the same demo built from a metric strip, a tree with counts, a
   table with badges and an evidence list reads as software.

   Reference for this vocabulary: SPREAD's product screens, which carry entity
   counts, source trees, extraction tables, a symptom-to-cause stepper and a
   "ruled out" panel — flat, neutral, almost no whitespace, accent only on
   status. That restraint is the point. Density is not decoration: every element
   here must carry information a real PULSAR screen would carry.

   HONESTY RULE FOR THIS SECTION. Numbers inside a demo describe the demo's own
   synthetic dataset (runs indexed, components observed, levels available) and
   never a customer outcome. No invented confidence percentages, no warranty
   exposure, no time-saved figures — those would be unsourced claims wearing a
   product screenshot as cover. Rank candidates instead of scoring them.
   ========================================================================== */

/* — Metric strip: the "this system holds real volume" row — */
.pd-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: 1px;
  overflow: hidden;
  border: 1px solid var(--pd-line);
  border-radius: var(--pd-radius-sm);
  background: var(--pd-line);
}

.pd-metric {
  padding: 9px 11px;
  background: var(--pd-raised);
}

.pd-metric__value {
  display: block;
  font-family: var(--pd-mono);
  font-size: 1.02rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--pd-text);
}

.pd-metric__value--data { color: var(--pd-data); }

.pd-metric__label {
  display: block;
  margin-top: 3px;
  font-family: var(--pd-mono);
  font-size: 0.56rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--pd-text-dim);
}

/* — Sidebar counts: a nav item that knows how much is behind it — */
.pd-nav__count {
  margin-left: auto;
  font-family: var(--pd-mono);
  font-size: 0.58rem;
  color: var(--pd-text-dim);
}

/* — Tree: source lists, product architecture, anything hierarchical — */
.pd-tree { display: flex; flex-direction: column; gap: 1px; }

.pd-tree__item {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 6px;
  border-radius: 3px;
  font-size: var(--pd-size-body);
  color: var(--pd-text-soft);
}

/* Depth by indent, set from the markup with a data attribute rather than a
   class per level — a tree that gains a level should not need new CSS. Goes to
   six because PULSAR's model has seven navigable abstraction levels, so a tree
   that walks it needs every one of them; stopping at three sent one flow off to
   restate the whole scale locally. A flow in a narrow column may still want a
   smaller step, which is a legitimate scoped override. */
.pd-tree__item[data-depth="1"] { padding-left: 18px; }
.pd-tree__item[data-depth="2"] { padding-left: 32px; }
.pd-tree__item[data-depth="3"] { padding-left: 46px; }
.pd-tree__item[data-depth="4"] { padding-left: 60px; }
.pd-tree__item[data-depth="5"] { padding-left: 74px; }
.pd-tree__item[data-depth="6"] { padding-left: 88px; }

.pd-tree__item.is-on { background: var(--pd-data-soft); color: var(--pd-text); }

/* OPT OUT OF THE TEAL WASH.
   `.is-on` carries two different meanings across these demos: "this is the one
   selected row" and "this element is part of what the current beat lights". The
   wash is right for the first and wrong for the second — when six tree rows or
   four table rows light in turn, washing them all spends the accent and leaves
   nothing standing out. Three flows had to override this locally before it was
   worth an opt-out. Use these when a beat lights many rows, and let the accent
   go to the one row that is actually the point. */
.pd-tree--quiet .pd-tree__item.is-on,
.pd-table--quiet tr.is-on td {
  background: none;
  color: var(--pd-text);
}

/* Disclosure caret. Rotates when its item is open. */
.pd-tree__caret {
  flex: none;
  width: 0;
  height: 0;
  border-left: 4px solid currentColor;
  border-top: 3px solid transparent;
  border-bottom: 3px solid transparent;
  opacity: 0.55;
  transition: transform var(--pd-t) var(--pd-ease);
}

.pd-tree__item.is-open .pd-tree__caret { transform: rotate(90deg); }

.pd-tree__count {
  margin-left: auto;
  font-family: var(--pd-mono);
  font-size: 0.58rem;
  color: var(--pd-text-dim);
}

/* — Badge: inline status inside a row or a table cell — */
.pd-badge {
  padding: 1px 6px;
  border: 1px solid currentColor;
  border-radius: 3px;
  font-family: var(--pd-mono);
  font-size: 0.55rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}

.pd-badge--neutral { color: var(--pd-text-dim); }
.pd-badge--data { color: var(--pd-data); }
.pd-badge--action { color: var(--pd-action); }
.pd-badge--fail { color: var(--pd-fail); }

/* — Table: the densest honest element available — */
.pd-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--pd-size-body);
}

.pd-table th {
  padding: 5px 8px;
  border-bottom: 1px solid var(--pd-line-strong);
  font-family: var(--pd-mono);
  font-size: 0.56rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  text-align: left;
  color: var(--pd-text-dim);
  white-space: nowrap;
}

.pd-table td {
  padding: 6px 8px;
  border-bottom: 1px solid var(--pd-line);
  color: var(--pd-text-soft);
  white-space: nowrap;
}

.pd-table tr.is-on td { background: var(--pd-data-soft); color: var(--pd-text); }
.pd-table td.pd-num { font-family: var(--pd-mono); font-size: var(--pd-size-meta); }

/* — Activity feed: timestamped events, the cheapest "this thing is live" cue — */
.pd-feed { display: flex; flex-direction: column; gap: 6px; }

.pd-feed__item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: var(--pd-size-meta);
  color: var(--pd-text-soft);
}

.pd-feed__time {
  flex: none;
  font-family: var(--pd-mono);
  font-size: 0.6rem;
  color: var(--pd-text-dim);
}

/* — Horizontal stepper: symptom to cause, as a chain of stages — */
.pd-steps {
  display: flex;
  align-items: stretch;
  gap: 6px;
}

.pd-step {
  flex: 1 1 0;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--pd-line);
  border-radius: var(--pd-radius-sm);
  background: var(--pd-raised);
  transition: border-color var(--pd-t) var(--pd-ease), background var(--pd-t) var(--pd-ease);
}

.pd-step.is-on { border-color: rgba(78, 205, 196, 0.45); background: var(--pd-data-soft); }
.pd-step.is-origin { border-color: var(--pd-fail); background: rgba(214, 69, 80, 0.14); }

.pd-step__kind {
  display: block;
  font-family: var(--pd-mono);
  font-size: 0.54rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--pd-text-dim);
}

.pd-step__what {
  display: block;
  margin-top: 3px;
  font-size: var(--pd-size-body);
  font-weight: 600;
  color: var(--pd-text);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* — Ruled out: what the system considered and discarded.
     This is the most valuable borrowing from the reference. It shows reasoning
     rather than asserting it, and it is honest — PULSAR ranks candidate origins,
     so naming the ones it set aside is exactly what it does. — */
.pd-ruled { display: flex; flex-direction: column; gap: 4px; }

.pd-ruled__item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: var(--pd-size-meta);
  color: var(--pd-text-dim);
}

.pd-ruled__item::before {
  content: "×";
  flex: none;
  font-family: var(--pd-mono);
  font-weight: 700;
  color: var(--pd-text-dim);
}

.pd-ruled__why { margin-left: auto; font-family: var(--pd-mono); font-size: 0.58rem; }

/* — Evidence list: one row per supporting fact, each with its own affordance.
     Per item, deliberately — a single "show evidence" button at the bottom of a
     finding is not the same claim as every line being traceable. — */
.pd-evidence { display: flex; flex-direction: column; gap: 5px; }

.pd-evidence__item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 8px;
  border: 1px solid var(--pd-line);
  border-radius: 3px;
  background: var(--pd-sunken);
  font-size: var(--pd-size-body);
  color: var(--pd-text-soft);
}

.pd-evidence__kind {
  flex: none;
  width: 58px;
  font-family: var(--pd-mono);
  font-size: 0.54rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--pd-text-dim);
}

.pd-evidence__what { flex: 1 1 auto; min-width: 0; color: var(--pd-text); }

.pd-evidence__drill {
  flex: none;
  font-family: var(--pd-mono);
  font-size: 0.56rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--pd-data);
}

.pd-evidence__item.is-on { border-color: rgba(78, 205, 196, 0.4); }

/* — Sparkline: a run of bars. Cheap, honest density for per-run measures. — */
.pd-spark {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 22px;
}

.pd-spark i {
  flex: 1 1 0;
  min-width: 2px;
  background: var(--pd-line-strong);
  border-radius: 1px 1px 0 0;
  transition: background var(--pd-t) var(--pd-ease);
}

.pd-spark i.is-flagged { background: var(--pd-action); }
.pd-spark i.is-on { background: var(--pd-data); }

/* — Toolbar: the filter row every list view has — */
.pd-toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

/* — Body layouts. A real screen is columns, not one canvas. — */
.pd-cols {
  display: grid;
  gap: var(--pd-gap);
  height: 100%;
  min-height: 0;
}

.pd-cols--2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.pd-cols--side { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
.pd-cols--3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1fr); }

.pd-stack { display: flex; flex-direction: column; gap: var(--pd-gap); min-height: 0; }

@media (max-width: 760px) {
  .pd-cols--2,
  .pd-cols--side,
  .pd-cols--3 { grid-template-columns: minmax(0, 1fr); }
  /* Below this width a three-column tool would be unreadable; the flows drop
     their supporting columns rather than shrinking all three. */
  .pd-cols--3 > *:nth-child(3) { display: none; }
}

/* ==========================================================================
   § REVEAL AND STATE HELPERS
   ========================================================================== */

.pd-enter {
  opacity: 0;
  transform: translateY(5px);
  transition: opacity var(--pd-t) var(--pd-ease), transform var(--pd-t) var(--pd-ease);
}

.pd-enter.is-on { opacity: 1; transform: none; }

/* Fold: takes no vertical room until its beat, then opens.

   USAGE: wrap exactly ONE child, put .pd-enter on that child, list both in the
   step's `on` array. The fold gives the room; the child gives the reveal.

   THE ROOM IS NOT ANIMATED, DELIBERATELY. Transitioning grid-template-rows
   from 0fr to 1fr interpolates to zero in Chrome once the fold sits in a flex
   column with a padded child — the region stays collapsed forever. Isolated
   probes confirmed the state logic is fine and the transition alone is at
   fault. Do not add one back without testing that exact case.

   CAVEAT: a collapsed fold still receives its parent's gap, and the primitive
   cannot know the parent's gap to cancel it. Cancel it in the flow. */
.pd-fold {
  display: grid;
  grid-template-rows: 0fr;
}

.pd-fold > * { overflow: hidden; min-height: 0; }
.pd-fold.is-on { grid-template-rows: 1fr; }

.pd-dim { opacity: 0.34; transition: opacity var(--pd-t) var(--pd-ease); }
.pd-dim.is-on { opacity: 1; }

/* A drawn line that traces itself. Set pathLength="100" in the markup so this
   works at any real path length without per-path dash maths. */
.pd-trace {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset var(--pd-t-slow) var(--pd-ease);
}

.pd-trace.is-on { stroke-dashoffset: 0; }

/* The travelling pulse. Only ever ONE per demo — it is the reader's eye. */
.pd-pulse {
  fill: var(--pd-data);
  opacity: 0;
  transition: opacity var(--pd-t-fast) linear;
}

.pd-pulse.is-on {
  opacity: 1;
  animation: pd-travel 2.2s var(--pd-ease) infinite;
}

@keyframes pd-travel {
  from { offset-distance: 0%; }
  to { offset-distance: 100%; }
}

/* A working indicator: three dots, for the moment the system is thinking. */
.pd-working { display: inline-flex; gap: 3px; }

.pd-working i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--pd-data);
  animation: pd-think 1.1s var(--pd-ease) infinite;
}

.pd-working i:nth-child(2) { animation-delay: 0.15s; }
.pd-working i:nth-child(3) { animation-delay: 0.3s; }

@keyframes pd-think {
  0%, 100% { opacity: 0.25; transform: translateY(0); }
  50% { opacity: 1; transform: translateY(-2px); }
}

/* Quiet ambient motion when the timeline is not running. */
.pd[data-pd-running="false"] .pd-pulse,
.pd[data-pd-running="false"] .pd-working i { animation-play-state: paused; }

/* …except where the movement IS the subject. A flow with `liveOutcome: true`
   keeps its pulse travelling after the last beat: the Behavioural Twin panel is
   about a signal crossing a domain boundary, and a dot frozen on a finished
   path says the opposite. Same specificity as the rule above, so it has to come
   after it. */
.pd[data-pd-live="true"] .pd-pulse { animation-play-state: running; }

@media (prefers-reduced-motion: reduce) {
  .pd[data-pd-live="true"] .pd-pulse { animation-play-state: paused; }
}

/* ==========================================================================
   § RESPONSIVE
   The sidebar is the first thing to go: on a phone a real app hides it too.
   ========================================================================== */

@media (max-width: 820px) {
  .pd { --pd-side-w: 0px; }
  .pd__app { grid-template-columns: minmax(0, 1fr); }
  .pd-app__side { display: none; }
}

@media (max-width: 640px) {
  .pd { --pd-pad: 11px; }
  /* Tighter tabs at phone width, so a five-beat strip needs less scrolling
     before it needs any. */
  .pd-tab { padding: 7px 8px; letter-spacing: 0.07em; }
  .pd--h-md .pd__app { min-height: 340px; }
  .pd--h-lg .pd__app { min-height: 380px; }
  .pd__narration { min-height: 4.5em; font-size: 0.84rem; }
  /* The pointer is a mouse. There is no mouse on a phone, and a fake cursor
     on a touch screen reads as a bug rather than as somebody demonstrating. */
  .pd__pointer { display: none; }
}

/* ==========================================================================
   § REDUCED MOTION
   Not "less animation" — none. The engine stops the timeline and settles on
   the final beat, so a visitor who asked for stillness gets the whole picture
   rather than the first frame of a story that never advances.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .pd *,
  .pd *::before,
  .pd *::after {
    transition-duration: 1ms !important;
    animation: none !important;
  }
  .pd-enter { opacity: 1; transform: none; }
  .pd-trace { stroke-dashoffset: 0; }
  .pd__pointer { display: none; }
  .pd-caret { display: none; }
}

/* ==========================================================================
   SYNTHETIC-DATA NOTE
   Every product surface on this site is a demonstration built on invented
   data. The note says so visibly, at the smallest size that still reads,
   wherever one of those surfaces appears. .pd-synth sits inside a demo's
   status bar; .pd-synth--fig stands under a figure that has no status bar.
   ========================================================================== */

/* Part of the meta layer: this is the site labelling the picture, not
   something the picture says about itself, so it speaks in violet too. */
.pd-synth {
  font-family: var(--pd-mono, var(--font-mono, "IBM Plex Mono", ui-monospace, monospace));
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--pd-live, #a78bfa);
  opacity: 0.85;
  white-space: nowrap;
}

.pd-app__status .pd-synth {
  margin-left: auto;
  padding-left: 16px;
}

.pd-synth--fig {
  display: block;
  margin: 9px 0 0;
  text-align: right;
}

/* ==========================================================================
   § THE TRANSPORT  ·  play/pause and replay, INSIDE the frame
   Play first, then replay, with the glyphs of a video player: it is the one
   control vocabulary every visitor already owns, and putting it on the player
   rather than under it is what connects it to the thing it drives.
   ========================================================================== */

.pd__controls {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Sitting in the status bar, the transport has to be quieter than the row it
   joins is loud — it is chrome inside chrome. */
.pd__controls--inset {
  margin-right: 4px;
  padding-right: 10px;
  border-right: 1px solid var(--pd-line);
}

.pd__ctl {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  /* Visible AT REST, not on hover. An affordance you only discover by
     accidentally pointing at it is not an affordance. */
  border: 1px solid var(--pd-live-line);
  background: var(--pd-live-soft);
  border-radius: 999px;
  font-family: var(--pd-mono);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--pd-live);
  cursor: pointer;
  transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
}

.pd__ctl:hover {
  color: #c7b6ff;
  border-color: var(--pd-live);
  background: rgba(167, 139, 250, 0.26);
}

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

.pd__ctl:focus-visible {
  outline: 2px solid var(--pd-live);
  outline-offset: 2px;
}

/* The glyphs are drawn in CSS rather than loaded: two shapes, and a font
   request for two shapes is a request too many. */
.pd__ctl-icon {
  width: 8px;
  height: 9px;
  flex: none;
  background: currentColor;
}

/* Play: a right-pointing triangle, cut from the block with clip-path.
   Pause: two bars, painted as a gradient rather than clipped — a single
   clip-path cannot describe two disjoint shapes, and the nine-point polygon
   that tried rendered as one notched block. */
.pd__ctl--play .pd__ctl-icon {
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

.pd__ctl--play.is-playing .pd__ctl-icon {
  clip-path: none;
  background: linear-gradient(
    to right,
    currentColor 0 32%,
    transparent 32% 68%,
    currentColor 68% 100%
  );
}

/* Replay: a ring with a bite out of it, plus the arrowhead. */
.pd__ctl--replay .pd__ctl-icon {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: none;
  border: 1.5px solid currentColor;
  border-right-color: transparent;
  position: relative;
}

.pd__ctl--replay .pd__ctl-icon::after {
  content: "";
  position: absolute;
  top: -1.5px;
  right: -1px;
  width: 0;
  height: 0;
  border-left: 3px solid currentColor;
  border-top: 2px solid transparent;
  border-bottom: 2px solid transparent;
}

/* Once it has finished, replay is the live move and should look like it. */
.pd[data-pd-done="true"] .pd__ctl--replay {
  border-color: var(--pd-live);
  background: rgba(167, 139, 250, 0.28);
  color: #d6cbff;
}

/* The legend that teaches the rule above. Quiet enough to read once and then
   stop noticing — it is scaffolding for a first visit, not a caption. */
.pd__legend {
  margin: 8px 0 0;
  font-family: var(--pd-mono);
  font-size: 0.58rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--pd-live-ink, #5b4bb8);
  opacity: 0.8;
}

.band--dark .pd__legend { color: #b3a3f5; }

/* A swatch in front of the sentence, so the rule is shown as well as stated:
   this colour, these controls. */
.pd__legend-key {
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 7px;
  vertical-align: -1px;
  border-radius: 999px;
  border: 1px solid var(--pd-live, #a78bfa);
  background: rgba(167, 139, 250, 0.3);
}
