/* ==========================================================================
   Flow Diagram 1.0 — an animated architecture diagram, three layouts
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   Three diagrams share one shell:

     hub     sources on the left, targets on the right, one core in the
             middle, curved wires between them, dots travelling along
     tree    a decision tree, one root-to-leaf path lit at a time
     chain   a request hop by hop, each link filling and reporting a timing

   WITHOUT JAVASCRIPT
   The dock is made of native radios and one native checkbox, so switching
   diagram and switching theme are pure CSS — they work with the script
   removed, and they work from the keyboard for free. The wires are stretched
   <svg> boxes, so they stay exactly on their anchors at any size, again with
   no script. What the script adds is motion: the travelling dots, the path
   that lights up step by step, the links that fill.

   PROPORTIONS
   Every metric derives from --fd-font-size, the title size of a node. The
   ratios were measured on the rendered reference at four widths — 1200x850,
   1440x900, 1920x1100 and 3830x2014 — so that a value is only called fixed
   when it held at all four (see _mesures/ratios.md). The base confirms
   itself: the structural numbers land on it exactly.

     node title      1                 (the reference unit, 14 px)
     wire anchor     16 em             (224 px, from the outer edge)
     scene inset     8 em              (112 px, top and bottom)
     row pitch       (height - 16em)/4 (the formula, verified at all four sizes)
     node            14.286 x 4.571 em (200 x 64 px), radius 1 em
     core            12 x 9.286 em     (168 x 130 px), radius 1.429 em
     tree node       10.286 x 4 em     (144 x 56 px), pitch 12 em / 7.714 em
     chain tile      4.571 em square   (64 px), pitch 11.429 em
     dock            6.5 em tall       (91 px + 1 px border each side)

   So changing --fd-font-size rescales the whole component, and nothing else
   needs to be touched. The scene itself stays fluid: it is a diagram, it is
   meant to fill the space it is given.

   Order of this file:
     1. Design tokens
     2. Shell and scene
     3. Hub: nodes, core, wires, travelling dots
     4. Tree: nodes and edges
     5. Chain: tiles, links, status line
     6. Dock: theme switch and diagram radios
     7. Switching between diagrams
     8. Motion
     9. Narrow screens
    10. Fallbacks: reduced motion, forced colours, no backdrop-filter

   Naming follows BEM: .flow-diagram, .flow-diagram__part,
   .flow-diagram--variant.
   ========================================================================== */

/* ==========================================================================
   1. Design tokens
   --------------------------------------------------------------------------
   The whole palette is one ink colour at a dozen opacities. Swapping the two
   lines under [data-fd-theme="light"] is the entire theme change — which is
   why the switch can be pure CSS.
   ========================================================================== */

.flow-diagram {
  /* Scale. Everything below is em, so this is the only size that matters. */
  --fd-font-size: 14px;

  /* The monospace stack of the reference, kept as measured. ui-monospace is
     deliberately absent: it resolves to a different face on Linux, and the
     advance width then no longer matches. Every family below advances at
     0.6 em, which is what the measured label widths rely on. */
  --fd-mono: "SF Mono", "JetBrains Mono", Menlo, monospace;

  /* Colour */
  --fd-ink: 255 255 255;
  --fd-surface: #0d0d0d;
  --fd-accent: #7c9cff;
  --fd-accent-glow: rgb(124 156 255 / 20%);
  --fd-ok: #3fb950;
  --fd-fail: #f85149;
  --fd-tooltip-bg: rgb(20 20 20 / 95%);
  --fd-tooltip-border: rgb(var(--fd-ink) / 15%);

  /* Ink levels, in the order they appear in the drawing */
  --fd-title: rgb(var(--fd-ink) / 92%);
  --fd-sub: rgb(var(--fd-ink) / 42%);
  --fd-node-bg: rgb(var(--fd-ink) / 4%);
  --fd-node-border: rgb(var(--fd-ink) / 12%);
  --fd-tile-bg: rgb(var(--fd-ink) / 6%);
  --fd-core-bg: rgb(var(--fd-ink) / 3%);
  --fd-core-border: rgb(var(--fd-ink) / 18%);
  --fd-wire: rgb(var(--fd-ink) / 16%);
  --fd-anchor-fill: rgb(var(--fd-ink) / 85%);
  --fd-edge: rgb(var(--fd-ink) / 14%);

  /* Geometry of the scene */
  --fd-gutter: 1.714em;                          /* 24 px  outer margin */
  --fd-anchor: 16em;                             /* 224 px wire anchor */
  --fd-inset-y: 8em;                             /* 112 px top and bottom */
  --fd-pitch: calc((100% - 2 * var(--fd-inset-y)) / 4);

  /* Node */
  --fd-node-w: 14.286em;                         /* 200 px */
  --fd-node-h: 4.571em;                          /* 64 px */
  --fd-node-radius: 1em;                         /* 14 px */
  --fd-tile: 2.286em;                            /* 32 px */
  --fd-tile-radius: 0.643em;                     /* 9 px */

  /* Core */
  --fd-core-w: 12em;                             /* 168 px */
  --fd-core-h: 9.286em;                          /* 130 px */
  --fd-core-radius: 1.429em;                     /* 20 px */
  --fd-core-glow:
    0 0 5.714em rgb(var(--fd-ink) / 8%),
    inset 0 0 2.857em rgb(var(--fd-ink) / 2%);
  --fd-core-halo:
    drop-shadow(0 0 0.857em rgb(var(--fd-ink) / 90%))
    drop-shadow(0 0 1.714em rgb(var(--fd-ink) / 90%));

  /* Wires */
  --fd-wire-width: 0.089em;                      /* 1.25 px, non-scaling */
  --fd-dot-r: 0.143em;                           /* 2 px */
  --fd-anchor-r: 0.25em;                         /* 3.5 px */

  /* Durations — measured, not guessed. See _mesures/ratios.md, section I. */
  --fd-cycle: 2857ms;                            /* one lap of a dot */
  --fd-step: 562ms;                              /* tree: one segment */
  --fd-hold: 3455ms;                             /* tree: before it clears */
  --fd-lap: 4017ms;                              /* tree: full cycle */
  --fd-fill: 560ms;                              /* chain: one link */
  --fd-delay: 2050ms;                            /* chain: before the verdict */
  --fd-rewind: 2450ms;                           /* chain: verdict to restart */
  --fd-t-theme: 0.4s;
  --fd-t-node: 0.3s;
  --fd-t-tab: 0.2s;
  --fd-t-switch: 0.25s;

  /* The component declares its own box model: it assumes no reset. */
  box-sizing: border-box;
  position: relative;
  display: block;
  width: 100%;
  min-height: 34.286em;                          /* below this the rows touch */
  aspect-ratio: 8 / 5;
  overflow: hidden;
  border-radius: var(--fd-core-radius);
  background: var(--fd-surface);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif;
  font-size: var(--fd-font-size);
  line-height: 1.15;
  transition: background var(--fd-t-theme);
}

.flow-diagram *,
.flow-diagram *::before,
.flow-diagram *::after {
  box-sizing: border-box;
}

/* The light theme. Two colour lines and three accents — nothing else moves. */
.flow-diagram[data-fd-theme="light"],
.flow-diagram:has(.flow-diagram__theme-input:checked) {
  --fd-ink: 13 13 13;
  --fd-surface: #f2f2f0;
  --fd-accent: #3457d5;
  --fd-accent-glow: rgb(52 87 213 / 20%);
  --fd-ok: #16a34a;
  --fd-tooltip-bg: rgb(255 255 255 / 97%);
  --fd-tooltip-border: rgb(var(--fd-ink) / 12%);
  --fd-title: rgb(var(--fd-ink) / 90%);
  --fd-node-bg: rgb(var(--fd-ink) / 3%);
  --fd-tile-bg: rgb(var(--fd-ink) / 5%);
  --fd-core-bg: rgb(var(--fd-ink) / 2.4%);
  --fd-wire: rgb(var(--fd-ink) / 14%);
  --fd-anchor-fill: rgb(var(--fd-ink) / 80%);
  --fd-core-glow:
    0 0 5.714em rgb(var(--fd-ink) / 6%),
    inset 0 0 2.857em rgb(var(--fd-ink) / 1.5%);
  --fd-core-halo:
    drop-shadow(0 0 0.857em rgb(var(--fd-ink) / 25%))
    drop-shadow(0 0 1.714em rgb(var(--fd-ink) / 15%));
}

/* ==========================================================================
   2. Shell and scene
   ========================================================================== */

.flow-diagram__stage {
  position: absolute;
  inset: 0;
}

.flow-diagram__view {
  position: absolute;
  inset: 0;
}

/* ==========================================================================
   3. Hub
   --------------------------------------------------------------------------
   Rows are numbered from the middle — row 0 is the centre line — and the
   pitch between them is the measured formula (height - 16em) / 4. A
   percentage inside a custom property resolves where it is used, so the same
   --fd-pitch serves both `top` and `height`.

   The row is an attribute, not an inline style: under a strict
   Content-Security-Policy the browser drops style attributes, and a layout
   that lives in them collapses. So each row gets its own rule.
   ========================================================================== */

.flow-diagram__node {
  position: absolute;
  top: 50%;
  display: flex;
  gap: 0.857em;                                  /* 12 px */
  align-items: center;
  width: var(--fd-node-w);
  height: var(--fd-node-h);
  padding: 0 1em;                                /* 14 px */
  transform: translateY(-50%);
  border: 1px solid var(--fd-node-border);
  border-radius: var(--fd-node-radius);
  background: var(--fd-node-bg);
  backdrop-filter: blur(0.429em);                /* 6 px */
  transition:
    border-color var(--fd-t-node),
    background var(--fd-t-node);
}

.flow-diagram__node[data-fd-side="in"] { left: var(--fd-gutter); }
.flow-diagram__node[data-fd-side="out"] { right: var(--fd-gutter); }

.flow-diagram__node[data-fd-row="-2"] { top: calc(50% - 2 * var(--fd-pitch)); }
.flow-diagram__node[data-fd-row="-1"] { top: calc(50% - var(--fd-pitch)); }
.flow-diagram__node[data-fd-row="0"] { top: 50%; }
.flow-diagram__node[data-fd-row="1"] { top: calc(50% + var(--fd-pitch)); }
.flow-diagram__node[data-fd-row="2"] { top: calc(50% + 2 * var(--fd-pitch)); }

.flow-diagram__tile {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--fd-tile);
  height: var(--fd-tile);
  border-radius: var(--fd-tile-radius);
  background: var(--fd-tile-bg);
  color: var(--fd-title);
}

.flow-diagram__tile svg {
  width: 1.286em;                                /* 18 px */
  height: 1.286em;
}

.flow-diagram__text {
  min-width: 0;                                  /* so the ellipsis can work */
}

.flow-diagram__title {
  display: block;
  overflow: hidden;
  color: var(--fd-title);
  font-size: 1em;
  font-weight: 500;
  line-height: 1.143;                            /* 16 px on 14 */
  white-space: nowrap;
  text-overflow: ellipsis;
}

.flow-diagram__sub {
  display: block;
  overflow: hidden;
  color: var(--fd-sub);
  font-size: 0.857em;                            /* 12 px */
  line-height: 1.167;                            /* 14 px on 12 */
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* The core ------------------------------------------------------------- */

.flow-diagram__core {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  flex-direction: column;
  gap: 0.714em;                                  /* 10 px */
  align-items: center;
  justify-content: center;
  width: var(--fd-core-w);
  height: var(--fd-core-h);
  transform: translate(-50%, -50%);
  border: 1px solid var(--fd-core-border);
  border-radius: var(--fd-core-radius);
  background: var(--fd-core-bg);
  box-shadow: var(--fd-core-glow);
  backdrop-filter: blur(0.571em);                /* 8 px */
  text-align: center;
}

.flow-diagram__core-icon {
  display: block;
  color: rgb(var(--fd-ink) / 90%);
  filter: var(--fd-core-halo);
}

.flow-diagram__core-icon svg {
  display: block;
  width: 2.286em;                                /* 32 px */
  height: 2.286em;
}

.flow-diagram__core .flow-diagram__title {
  color: rgb(var(--fd-ink) / 95%);
  font-size: 1.071em;                            /* 15 px */
  font-weight: 600;
  line-height: 1.133;
}

.flow-diagram__core .flow-diagram__sub {
  color: rgb(var(--fd-ink) / 50%);
  line-height: 1.333;
}

/* Wires ----------------------------------------------------------------
   One shape for every curve. The <svg> is stretched to its box with
   preserveAspectRatio="none", so the path — normalised to a 100x100 grid —
   lands exactly on the two anchors whatever the size of the scene. The
   stroke keeps its width because of vector-effect. This is what makes the
   diagram correct with the script removed.
   ---------------------------------------------------------------------- */

.flow-diagram__wire {
  position: absolute;
  height: var(--fd-pitch);
  overflow: visible;
  fill: none;
  stroke: var(--fd-wire);
  stroke-width: var(--fd-wire-width);
  pointer-events: none;
}

/* The width is written out rather than left to `left` + `right`: an <svg> is
   a replaced element with an intrinsic ratio, so it resolves its own width
   from the viewBox and quietly ignores the pair. That mistake made every
   curve 169 px wide instead of 412. */
.flow-diagram__wire[data-fd-side="in"],
.flow-diagram__wire[data-fd-side="out"] {
  width: calc(50% - var(--fd-core-w) / 2 - var(--fd-anchor));
}

.flow-diagram__wire[data-fd-side="in"] { left: var(--fd-anchor); }

.flow-diagram__wire[data-fd-side="out"] {
  right: var(--fd-anchor);
  transform: scaleX(-1);
}

/* A wire that climbs to the core is the same curve upside down. */
.flow-diagram__wire[data-fd-row="-1"] {
  top: calc(50% - var(--fd-pitch));
}

.flow-diagram__wire[data-fd-row="1"] {
  top: 50%;
  transform: scaleY(-1);
}

.flow-diagram__wire[data-fd-side="out"][data-fd-row="1"] {
  transform: scale(-1, -1);
}

/* The flat wire has no height to stretch into, so it gets its own box. */
.flow-diagram__wire[data-fd-row="0"] {
  top: 50%;
  height: 0.143em;
  transform: translateY(-50%);
}

.flow-diagram__wire[data-fd-side="out"][data-fd-row="0"] {
  transform: translateY(-50%) scaleX(-1);
}

/* The anchor dot. The wire starts at 16 em from the outer edge, which is
   exactly the inner edge of the node — so the dot is a pseudo-element of the
   node and there is nothing to keep in sync. */
.flow-diagram__node::after {
  content: "";
  position: absolute;
  top: 50%;
  width: calc(2 * var(--fd-anchor-r));
  height: calc(2 * var(--fd-anchor-r));
  border-radius: 50%;
  background: var(--fd-anchor-fill);
}

.flow-diagram__node[data-fd-side="in"]::after {
  right: 0;
  translate: 50% -50%;
}

.flow-diagram__node[data-fd-side="out"]::after {
  left: 0;
  translate: -50% -50%;
}

/* The travelling dots. The script writes their position in em on the two
   custom properties; with no script the layer stays empty and the diagram
   is simply still. */
.flow-diagram__dots {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.flow-diagram__dot {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(2 * var(--fd-dot-r));
  height: calc(2 * var(--fd-dot-r));
  margin: calc(-1 * var(--fd-dot-r)) 0 0 calc(-1 * var(--fd-dot-r));
  translate: var(--fd-x, 0) var(--fd-y, 0);
  border-radius: 50%;
  background: var(--fd-anchor-fill);
  opacity: 0.7;
}

/* ==========================================================================
   4. Tree
   --------------------------------------------------------------------------
   The block has a fixed size in em, so its ratio never changes — which means
   the edges can live in one <svg> in plain coordinates and still be exact.
   No stretching, no per-edge box.

   The nodes are placed on a grid: data-fd-col counts 3 em steps (42 px) from
   the left edge, data-fd-row counts rows. Every measured centre lands on that
   grid — 84, 168, 252, 294, 420 px are 2, 4, 6, 7 and 10 steps.

   The layout rule is one sentence: a parent sits at the average of its
   children. Writing it into the markup is what keeps the tree correct with
   the script removed.
   ========================================================================== */

.flow-diagram__tree {
  --fd-col-unit: 3em;                            /* 42 px */
  --fd-row-unit: 7.714em;                        /* 108 px */

  position: absolute;
  top: 50%;
  left: 50%;
  width: 36em;                                   /* 504 px */
  height: 34.857em;                              /* 488 px */
  transform: translate(-50%, -50%);
}

.flow-diagram__tnode {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 10.286em;                               /* 144 px */
  height: 4em;                                   /* 56 px */
  padding: 0 0.714em;                            /* 10 px */
  transform: translateX(-50%);
  border: 1px solid var(--fd-node-border);
  border-radius: 0.857em;                        /* 12 px */
  background: var(--fd-node-bg);
  text-align: center;
  transition:
    border-color var(--fd-t-node),
    background var(--fd-t-node),
    box-shadow var(--fd-t-node);
}

.flow-diagram__tnode[data-fd-row="0"] { top: 0; }
.flow-diagram__tnode[data-fd-row="1"] { top: var(--fd-row-unit); }
.flow-diagram__tnode[data-fd-row="2"] { top: calc(2 * var(--fd-row-unit)); }
.flow-diagram__tnode[data-fd-row="3"] { top: calc(3 * var(--fd-row-unit)); }
.flow-diagram__tnode[data-fd-row="4"] { top: calc(4 * var(--fd-row-unit)); }

.flow-diagram__tnode[data-fd-col="0"] { left: 0; }
.flow-diagram__tnode[data-fd-col="1"] { left: var(--fd-col-unit); }
.flow-diagram__tnode[data-fd-col="2"] { left: calc(2 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="3"] { left: calc(3 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="4"] { left: calc(4 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="5"] { left: calc(5 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="6"] { left: calc(6 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="7"] { left: calc(7 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="8"] { left: calc(8 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="9"] { left: calc(9 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="10"] { left: calc(10 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="11"] { left: calc(11 * var(--fd-col-unit)); }
.flow-diagram__tnode[data-fd-col="12"] { left: calc(12 * var(--fd-col-unit)); }

.flow-diagram__tnode .flow-diagram__title {
  color: rgb(var(--fd-ink) / 88%);
  font-size: 0.893em;                            /* 12.5 px */
  font-weight: 600;
  line-height: 1.12;
}

.flow-diagram__tnode .flow-diagram__sub {
  color: rgb(var(--fd-ink) / 40%);
  font-family: var(--fd-mono);
  font-size: 0.75em;                             /* 10.5 px */
  line-height: 1.333;
}

/* A node on the lit path. */
.flow-diagram__tnode[data-fd-on] {
  border-color: var(--fd-accent);
  box-shadow: 0 0 1.571em var(--fd-accent-glow);
}

/* One svg for every edge, in the block's own coordinates (504 x 488). The
   block never changes ratio, so nothing is stretched and the lines land
   exactly where they were measured. The stroke widths scale with the
   component because the svg is sized in em. */
.flow-diagram__edges {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.flow-diagram__tedge {
  stroke: var(--fd-edge);
  stroke-width: 1.5;
  transition:
    stroke var(--fd-t-node),
    stroke-width var(--fd-t-node);
}

.flow-diagram__tedge[data-fd-on] {
  stroke: var(--fd-accent);
  stroke-width: 2.5;
}

/* ==========================================================================
   5. Chain
   --------------------------------------------------------------------------
   A flex row: tile, link, tile, link … The group — chain plus status line —
   is what gets centred, not the chain alone. That is why the block sits
   above the middle by half the status line.
   ========================================================================== */

.flow-diagram__chain-group {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  flex-direction: column;
  gap: 2em;                                      /* 28 px */
  align-items: center;
  transform: translate(-50%, -50%);
}

/* The padding is measured, not decorative slack: the reference block is
   184 x 802 around a 104 x 704 chain, and it is the padded block that gets
   centred. Drop it and the whole chain sits 16 px too high. */
.flow-diagram__chain {
  display: flex;
  align-items: flex-start;
  padding: 2.857em 3.429em;                      /* 40 px 48 px */
}

/* A hop is 64 px wide unless its own label is wider — which is how the last
   one measures 66 px on the reference. min-width, not width. */
.flow-diagram__hop {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  gap: 0.714em;                                  /* 10 px */
  align-items: center;
  width: max-content;
  min-width: 4.571em;                            /* 64 px */
  text-align: center;
}

.flow-diagram__hop-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 4.571em;
  height: 4.571em;
  border: 1px solid rgb(var(--fd-ink) / 40%);
  border-radius: 1.143em;                        /* 16 px */
  background: var(--fd-node-bg);
  box-shadow: 0 0 1.714em rgb(var(--fd-ink) / 6%);
  color: rgb(var(--fd-ink) / 90%);
  transition:
    border-color var(--fd-t-node),
    color var(--fd-t-node);
}

.flow-diagram__hop-tile svg {
  width: 1.714em;                                /* 24 px */
  height: 1.714em;
}

.flow-diagram__hop[data-fd-state="fail"] .flow-diagram__hop-tile {
  border-color: var(--fd-fail);
  color: var(--fd-fail);
}

.flow-diagram__hop .flow-diagram__title {
  color: rgb(var(--fd-ink) / 90%);
  font-size: 0.929em;                            /* 13 px */
  font-weight: 500;
  line-height: 1.154;
}

/* 11 px, not the 12 px of the other subtitles — and it is this line that
   sets the width of a hop: "Stripe API" at 11 px monospace measures exactly
   the 66 px the last node was measured at. */
.flow-diagram__hop .flow-diagram__sub {
  color: rgb(var(--fd-ink) / 40%);
  font-family: var(--fd-mono);
  font-size: 0.786em;                            /* 11 px */
  line-height: 1.364;                            /* 15 px */
}

/* The link between two hops. The rail is the track, --fd-progress the part
   already travelled: 0 before the script runs on that link, 1 after. With no
   script the markup ships them at 1, so the chain reads as a finished
   request. */
.flow-diagram__link {
  position: relative;
  flex-shrink: 0;
  width: 6.857em;                                /* 96 px */
  height: 0.143em;                               /* 2 px */
  margin-top: 3.643em;                           /* 51 px below the tile top */
  border-radius: 0.071em;
  background: rgb(var(--fd-ink) / 14%);
}

.flow-diagram__link::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: scaleX(var(--fd-progress, 1));
  transform-origin: left;
  border-radius: inherit;
  background: var(--fd-ok);
  /* No transition on transform: the script writes --fd-progress on every
     frame, and a transition on top would smooth an already smoothed value.
     The two fought each other — a link took 780 ms and then 1363 ms to fill
     instead of 560, and they all started at the same time. */
  transition: background var(--fd-t-tab);
}

.flow-diagram__link[data-fd-state="fail"]::after { background: var(--fd-fail); }
.flow-diagram__link[data-fd-state="idle"]::after { background: transparent; }

/* Careful with the em here: this element sets its own font-size to 11 px, so
   every length below is a ratio of 11, not of 14. Written against the outer
   size the padding comes out a fifth too small and the label 3.4 px narrow. */
.flow-diagram__note {
  position: absolute;
  bottom: 0.636em;                               /* 7 px above the rail */
  left: 50%;
  padding: 0.273em 0.727em;                      /* 3 px 8 px */
  transform: translateX(-50%);
  border: 1px solid var(--fd-tooltip-border);
  border-radius: 0.545em;                        /* 6 px */
  background: var(--fd-tooltip-bg);
  color: rgb(var(--fd-ink) / 95%);
  font-family: var(--fd-mono);
  font-size: 0.786em;                            /* 11 px */
  line-height: 1.364;                            /* 15 px */
  white-space: nowrap;
}

.flow-diagram__link[data-fd-state="fail"] .flow-diagram__note {
  border-color: var(--fd-fail);
  color: var(--fd-fail);
}

/* Same caution: 13 px is the local base. */
.flow-diagram__status {
  display: flex;
  gap: 0.615em;                                  /* 8 px */
  align-items: center;
  min-height: 1.846em;                           /* 24 px, as measured */
  margin: 0;
  font-family: var(--fd-mono);
  font-size: 0.929em;                            /* 13 px */
  line-height: 1.385;                            /* 18 px */
  opacity: var(--fd-status-opacity, 1);
  transition: opacity var(--fd-t-node);
}

.flow-diagram__status-dot {
  display: block;
  flex-shrink: 0;
  width: 0.538em;                                /* 7 px on the 13 px base */
  height: 0.538em;
  border-radius: 50%;
  background: var(--fd-fail);
}

.flow-diagram__status[data-fd-state="ok"] .flow-diagram__status-dot {
  background: var(--fd-ok);
}

.flow-diagram__status-label {
  color: rgb(var(--fd-ink) / 95%);
  font-weight: 600;
}

.flow-diagram__status-meta { color: rgb(var(--fd-ink) / 45%); }

/* ==========================================================================
   6. Dock
   --------------------------------------------------------------------------
   Native radios and a native checkbox, kept in the layout at 1 px and made
   transparent rather than display:none — a control that is not rendered
   cannot take focus, and the browser then refuses to show it at all. The
   labels carry the look; the inputs carry the behaviour, the roles and the
   keyboard.
   ========================================================================== */

.flow-diagram__dock {
  position: absolute;
  bottom: var(--fd-gutter);
  left: 50%;
  z-index: 2;
  display: flex;
  align-items: stretch;
  overflow: hidden;
  transform: translateX(-50%);
  border: 1px solid rgb(255 255 255 / 9%);
  border-radius: 1.143em;                        /* 16 px */
  background: rgb(14 14 14 / 88%);
  box-shadow:
    0 0.857em 2.857em rgb(0 0 0 / 50%),
    inset 0 1px 0 rgb(255 255 255 / 5%);
  backdrop-filter: blur(1.714em);                /* 24 px */
  user-select: none;
}

.flow-diagram__control {
  display: flex;
  flex-direction: column;
  gap: 0.571em;                                  /* 8 px */
  align-items: center;
  padding: 0.857em 1.571em;                      /* 12 px 22 px */
  cursor: pointer;
  transition: background var(--fd-t-tab);
}

.flow-diagram__control--theme { padding-inline: 1.429em; }  /* 20 px */

.flow-diagram__legend,
.flow-diagram__value {
  font-family: var(--fd-mono);
  text-align: center;
  white-space: nowrap;
}

/* The two line-heights are written out because they set the height of the
   whole dock: 14 + 8 + 24 + 8 + 13 of content, 12 of padding top and bottom,
   1 px of border each side — the 93 px that was measured. Left to the
   inherited 1.15 the bar comes out 5 px short. */
.flow-diagram__legend {
  color: rgb(255 255 255 / 38%);
  font-size: 0.714em;                            /* 10 px */
  font-weight: 600;
  line-height: 1.4;                              /* 14 px */
  letter-spacing: 0.08em;                        /* 0.8 px on 10 */
  transition: color var(--fd-t-tab);
}

.flow-diagram__value {
  color: rgb(255 255 255 / 25%);
  font-size: 0.643em;                            /* 9 px */
  line-height: 1.444;                            /* 13 px */
  letter-spacing: 0.06em;
}

.flow-diagram__separator {
  width: 1px;
  align-self: center;
  height: 5.071em;                               /* 71 px */
  background: rgb(255 255 255 / 7%);
}

/* The inputs. One pixel, transparent, still focusable. */
.flow-diagram__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
  appearance: none;
}

/* Focus lands on the input, so the ring is drawn on its label. */
.flow-diagram__input:focus-visible + .flow-diagram__control {
  outline: 2px solid var(--fd-accent);
  outline-offset: -2px;
}

/* Theme switch --------------------------------------------------------- */

.flow-diagram__track {
  position: relative;
  flex-shrink: 0;
  width: 3.143em;                                /* 44 px */
  height: 1.714em;                               /* 24 px */
  border-radius: 0.857em;                        /* 12 px */
  background: rgb(255 255 255 / 12%);
  transition: background var(--fd-t-switch);
}

.flow-diagram__knob {
  position: absolute;
  top: 0.214em;                                  /* 3 px */
  left: 0.214em;
  width: 1.286em;                                /* 18 px */
  height: 1.286em;
  border-radius: 50%;
  background: rgb(255 255 255 / 88%);
  box-shadow: 0 1px 4px rgb(0 0 0 / 30%);
  transition: left var(--fd-t-switch);
}

.flow-diagram__theme-input:checked + .flow-diagram__control .flow-diagram__knob {
  left: calc(3.143em - 1.286em - 0.214em);
}

.flow-diagram__theme-input:checked + .flow-diagram__control .flow-diagram__track {
  background: rgb(255 255 255 / 24%);
}

/* Diagram radios ------------------------------------------------------- */

/* A fieldset, so the three radios are a named group without inventing a
   role. Its default chrome is removed; min-inline-size is the one nobody
   remembers — a fieldset refuses to shrink below its content without it. */
.flow-diagram__tabs {
  display: flex;
  align-items: stretch;
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

/* Visible to a screen reader, to nothing else. */
.flow-diagram__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.flow-diagram__pip {
  width: 0.357em;                                /* 5 px */
  height: 0.357em;
  border-radius: 50%;
  background: transparent;
  transition: background var(--fd-t-tab);
}

.flow-diagram__mode-input:checked + .flow-diagram__control {
  background: rgb(255 255 255 / 8%);
}

.flow-diagram__mode-input:checked + .flow-diagram__control .flow-diagram__legend {
  color: rgb(255 255 255 / 90%);
}

.flow-diagram__mode-input:checked + .flow-diagram__control .flow-diagram__pip {
  background: rgb(255 255 255 / 70%);
}

@media (hover: hover) {
  .flow-diagram__control:hover { background: rgb(255 255 255 / 4%); }
  .flow-diagram__control:hover .flow-diagram__legend { color: rgb(255 255 255 / 70%); }
  .flow-diagram__mode-input:checked + .flow-diagram__control:hover {
    background: rgb(255 255 255 / 10%);
  }
}

/* ==========================================================================
   7. Switching between diagrams
   --------------------------------------------------------------------------
   Two selectors for one rule: :has() covers the no-script case, the data
   attribute covers the scripted one. Neither depends on the other.

   The match is on the radio's value, never on an id: a component that hard
   codes ids cannot be put on a page twice. Ids and name are yours to choose,
   and the script fills them in if you leave them out.
   ========================================================================== */

.flow-diagram__view { display: none; }

.flow-diagram:has(.flow-diagram__mode-input[value="hub"]:checked) [data-fd-view="hub"],
.flow-diagram:has(.flow-diagram__mode-input[value="tree"]:checked) [data-fd-view="tree"],
.flow-diagram:has(.flow-diagram__mode-input[value="chain"]:checked) [data-fd-view="chain"],
.flow-diagram[data-fd-view="hub"] [data-fd-view="hub"],
.flow-diagram[data-fd-view="tree"] [data-fd-view="tree"],
.flow-diagram[data-fd-view="chain"] [data-fd-view="chain"] {
  display: block;
}

/* No dock at all: the component holds a single diagram and shows it. This is
   the shape you want when you are illustrating one architecture and have no
   use for a switch. */
.flow-diagram:not(:has(.flow-diagram__mode-input)) .flow-diagram__view {
  display: block;
}

/* ==========================================================================
   8. Motion
   ========================================================================== */

.flow-diagram__view {
  animation: fd-appear var(--fd-t-node) ease-out both;
}

@keyframes fd-appear {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ==========================================================================
   9. Narrow screens
   --------------------------------------------------------------------------
   Under 640 px the hub cannot hold a 200 px card on each side of a 168 px
   core, so the scale drops. The layout is untouched: only --fd-font-size
   changes, which is the point of expressing everything in em.
   ========================================================================== */

@media (max-width: 900px) {
  .flow-diagram { --fd-font-size: 11px; }
}

/* Below 640 px, lowering the body copy is no longer enough: two 14.3 em cards
   plus a 12 em hub demand 44 em of width whatever happens, and the scene no longer
   has that much. So it is the pieces that shrink — the card, the hub, and the
   anchor with them, since the anchor is the edge of the card. The chain holds
   by shortening its links; its labels would be unreadable at that size
   anyway. */
@media (max-width: 640px) {
  .flow-diagram {
    --fd-font-size: 8px;
    --fd-node-w: 10em;
    --fd-anchor: 11.715em;                       /* marge + carte */
    --fd-core-w: 9em;
    aspect-ratio: 3 / 4;
  }

  .flow-diagram__link { width: 2.5em; }
  .flow-diagram__chain { padding-inline: 1em; }
  .flow-diagram__note { display: none; }

  .flow-diagram__dock { --fd-font-size: 11px; font-size: 11px; }
}

/* Below 420 px the control bar is wider than the scene and gets clipped. Its
   inner padding is closed up rather than its type: at that size, reducing the
   body copy would make the labels unreadable. */
@media (max-width: 420px) {
  .flow-diagram__control { padding-inline: 0.857em; }
  .flow-diagram__control--theme { padding-inline: 0.857em; }
}

@media (max-width: 380px) {
  .flow-diagram { --fd-font-size: 7px; }
}

/* ==========================================================================
   10. Fallbacks
   ========================================================================== */

/* Reduced motion: the delays go to zero too — a delay is motion spread over
   time. The diagrams keep their finished state, which is the readable one. */
@media (prefers-reduced-motion: reduce) {
  .flow-diagram,
  .flow-diagram * {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }
  .flow-diagram__dots { display: none; }
}

/* Forced colours: the ink opacities collapse to nothing, so borders and text
   are handed back to the system palette. */
@media (forced-colors: active) {
  .flow-diagram {
    --fd-title: CanvasText;
    --fd-sub: CanvasText;
    --fd-wire: CanvasText;
    --fd-anchor-fill: CanvasText;
    --fd-edge: CanvasText;
    --fd-accent: Highlight;
    background: Canvas;
  }
  .flow-diagram__node,
  .flow-diagram__core,
  .flow-diagram__tnode,
  .flow-diagram__hop-tile,
  .flow-diagram__note,
  .flow-diagram__dock,
  .flow-diagram__track {
    border: 1px solid CanvasText;
    background: Canvas;
  }
  .flow-diagram__legend,
  .flow-diagram__value,
  .flow-diagram__title,
  .flow-diagram__sub { color: CanvasText; }
  .flow-diagram__mode-input:checked + .flow-diagram__control {
    background: Highlight;
  }
  .flow-diagram__mode-input:checked + .flow-diagram__control .flow-diagram__legend {
    color: HighlightText;
  }
  .flow-diagram__link { background: CanvasText; }
  .flow-diagram__knob,
  .flow-diagram__pip { background: CanvasText; forced-color-adjust: none; }
  .flow-diagram__tedge[data-fd-on] { stroke: Highlight; }
}

/* No backdrop-filter: the frosted panels would read as flat 4 % washes on the
   background, which is nearly invisible. They get an opaque fill instead. */
@supports not (backdrop-filter: blur(1px)) {
  .flow-diagram__node,
  .flow-diagram__core { background: rgb(var(--fd-ink) / 8%); }
  .flow-diagram__dock { background: rgb(14 14 14 / 98%); }
}
