/* ==========================================================================
   rubtur.css — the RUBTUR Portal architecture.

   The picture is the case: the portal sits between somebody else's CRM and the
   manager's own mailbox, with an AI layer under it that turns one request into
   a pack of documents, and a cloud model it calls rather than embeds.

   Same division of labour as diagram.css. The boxes are text to read, so they
   stay still; what moves is what travels along each wire, carried by
   animateMotion along the very path the wire is drawn with. A dot where the
   traffic is just traffic, an envelope on the wire to Gmail, a page on the
   wire to the document pack, a token stream coming back from the model.

   The layout is a grid in which **every connector has its own cell** — the gap
   between two boxes *is* a cell, and the wire is stretched into it. That is
   what keeps a wire touching both boxes at any width, in either language, and
   however tall the text makes them. It is also why every wire is drawn in the
   same 100 × 100 box: one set of stroke rules covers all of them, and
   `vector-effect="non-scaling-stroke"` stops a wire in a tall cell coming out
   thicker than the same wire in a short one.

   Under 1000px the picture stops being a picture and becomes the same
   information as a readable list, exactly as the ADTech diagram does.
   ========================================================================== */

.rbt {
  /* Wide enough for the CRM wire's *whole* label, not just its longest word.
     At 112px a two-line label centred on this column spilled sixty pixels
     either side, over the client contour on one hand and over the CRM box's
     own note on the other — the one collision left on this figure. */
  --gap: clamp(150px, 13vw, 214px);
  /* Tall enough for a two-line wire label plus air above and below it: the
     labels either side of the drop are centred in this gap, and at 54px the
     longer of the two wrapped to four lines and ran into the box beneath. */
  --vgap: clamp(74px, 7vw, 104px);
  /* Where the portal box's centre falls in the left column: the vertical
     wires hang from it. Measured on the rendered figure — see ZONE_X in
     tools/rubtur.py, and re-measure if .rbt-zone__row's columns change. */
  --hang: 72.2%;

  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--gap) minmax(0, 0.66fr);
  align-items: stretch;
  margin: var(--s-5) 0 var(--s-6);
}
.rbt, .rbt figure { margin-inline: 0; }

/* Row 1 client-facing | wire | CRM · row 2 the drop · rows 3–4 the AI layer,
   beside one output each · row 5 the drop · row 6 the model. The right-hand
   column is empty on rows 2, 5 and 6, which is the shape of the thing: only
   the portal and the layer under it talk to anybody outside.

   The layer spans both output rows rather than fanning into them: two wires
   that keep their own ratio can carry an envelope and a page, and one fan
   stretched down a tall cell can only carry dots. */
.rbt__contour  { grid-area: 1 / 1; }
.rbt__gap--crm { grid-area: 1 / 2; }
.rbt__crm      { grid-area: 1 / 3; }
.rbt__vgap--ai { grid-area: 2 / 1; }
.rbt__hub      { grid-area: 3 / 1 / 5 / 2; }
.rbt__gap--t0  { grid-area: 3 / 2; }
.rbt__tool--0  { grid-area: 3 / 3; }
.rbt__gap--t1  { grid-area: 4 / 2; }
.rbt__tool--1  { grid-area: 4 / 3; }
.rbt__vgap--llm { grid-area: 5 / 1; }
.rbt__llm      { grid-area: 6 / 1; }

/* The two output rows split the layer's height between them, so each wire
   meets its box in the middle. */
.rbt { grid-template-rows: auto auto minmax(0, 1fr) minmax(0, 1fr) auto auto; }
.rbt__tool { display: grid; align-content: center; padding-bottom: var(--s-4); }
.rbt__tool--1 { padding-bottom: 0; padding-top: var(--s-4); }

/* --- boxes ---------------------------------------------------------------- */
.rbt-box {
  display: grid; grid-template-columns: 40px minmax(0, 1fr);
  gap: var(--s-4); align-items: start;
  padding: var(--s-4);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface);
}
.rbt-box .ico { width: 40px; height: 40px; stroke: var(--c, var(--c-blue)); fill: none; }
.rbt-box__h { margin: 0 0 5px; font-weight: 640; font-size: var(--fs-xs); letter-spacing: -.01em; color: var(--ink); }
.rbt-box p:last-child { margin: 0; font-size: var(--fs-3xs); line-height: 1.45; color: var(--ink-3); }
.rbt-box--sm { align-items: center; }        /* a name alone, centred on its mark */
/* Every mark is one size — the portal's — so no box reads as smaller than its
   neighbour by accident. The portal keeps a heavier heading, which is what
   makes it the lead rather than a bigger glyph. */
.rbt-box .ico { stroke-width: 1.6; }
.rbt-box--lead { align-items: center; }
.rbt-box--lead .rbt-box__h { font-size: var(--fs-sm); }

/* --- the client-facing frame --------------------------------------------- */
/* Dashed, because it is a boundary rather than a component: everything inside
   it is what the client's own people touch. */
.rbt-zone {
  position: relative; height: 100%;
  display: flex; align-items: center;
  padding: var(--s-6) var(--s-5) var(--s-5);   /* room for its own eyebrow */
  border: 1px dashed var(--line-strong); border-radius: var(--r-lg);
  background: color-mix(in oklab, var(--c, var(--c-blue)) 4%, transparent);
}
.rbt-zone__lab {
  position: absolute; top: var(--s-3); left: var(--s-5);
  font-family: var(--font-mono); font-size: var(--fs-3xs);
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3);
}
/* These three fractions are what --hang is measured from. */
.rbt-zone__row {
  width: 100%;
  display: grid; grid-template-columns: minmax(0, 1fr) 44px minmax(0, 1.35fr);
  align-items: center;
}

/* --- the AI layer, the heaviest thing in the picture ---------------------- */
/* This is the one box on the picture that is neither somebody else's product
   nor a wire: it is the part made of models, and it is what the case is for.
   So it gets a different kind of light — the brand ramp, at a fraction of its
   strength, as a hairline ring and a wash that fades out before it reaches
   any text.

   Subtlety is the whole point and it is not a matter of taste. The ramp is
   nine saturated colours; at anything like full strength a rainbow edge is
   the visual language of a crypto landing page, and the box would shout over
   the diagram it belongs to. At 40% on a 1px ring and 8% on a wash it reads
   as iridescence on a dark surface — you notice the box is special before you
   notice why. `isolation: isolate` keeps both layers' blending inside this
   box, and the contents are lifted over them explicitly, because an absolute
   pseudo-element otherwise paints over its own parent's text. */
.rbt__hub {
  position: relative; isolation: isolate;
  padding: var(--s-5);
  /* transparent, because ::before draws the edge as a gradient */
  border: 1px solid transparent; border-radius: var(--r-lg);
  background:
    radial-gradient(ellipse 80% 120% at 12% 50%,
                    color-mix(in oklab, var(--c, var(--c-blue)) 12%, transparent),
                    transparent 70%),
    var(--surface);
}
/* The ring. A gradient cannot be a border, so it is a padding-box-masked
   layer: the ramp fills the element, and the mask subtracts everything but
   the 1px frame. */
.rbt__hub::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  padding: 1px; border-radius: inherit;
  background: var(--brand-ramp);
  opacity: .75;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}
/* The ramp itself lives here, in the background of the layer rather than in a
   bloom around it: thrown from the top-left corner and gone by two-thirds
   across, so it never sits behind a line of type at full strength. */
.rbt__hub::after {
  content: ""; position: absolute; inset: 0; z-index: 0;
  border-radius: inherit;
  background: var(--brand-ramp);
  opacity: .3;
  -webkit-mask-image: linear-gradient(150deg, #000, transparent 64%);
          mask-image: linear-gradient(150deg, #000, transparent 64%);
  pointer-events: none;
}
.rbt__hub > * { position: relative; z-index: 1; }

/* The AI layer wears its mark the way the boxes wear theirs: a 40px column,
   then the text stacked beside it. Every mark on the figure is one size, and
   the sparkles is not an exception because it happens to sit over an eyebrow.
   The icon stays a sibling of the translated nodes and not a child of one:
   js/i18n.js swaps an element that has children by innerHTML, so an icon
   inside the label would not survive the first switch to Russian. */
.rbt-hub__top {
  display: grid; grid-template-columns: 40px minmax(0, 1fr);
  align-items: center; gap: var(--s-4);
  margin-bottom: var(--s-4);
}
.rbt-hub__txt { display: grid; gap: 4px; }
/* Sparkles: the mark for something a model did. There was no such glyph in the
   set; 'magic' fell through to the fallback, which is why this line once
   carried three dots. */
.rbt-hub__ico {
  width: 40px; height: 40px;
  fill: none; stroke: var(--c-violet); stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 7px color-mix(in oklab, var(--c-violet) 55%, transparent));
}
.rbt-hub__eyebrow {
  display: block;
  font-family: var(--font-mono); font-size: var(--fs-3xs);
  letter-spacing: .14em; text-transform: uppercase; color: var(--c, var(--c-blue));
}
.rbt-hub__h {
  margin: 0; font-weight: 700;
  font-size: clamp(16px, 1.25vw, 21px); letter-spacing: -.015em;
}
.rbt-hub__cells { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
.rbt-cell {
  padding: var(--s-4);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: rgba(9, 14, 26, .5);
}
.rbt-cell__h { margin: 0 0 5px; font-weight: 640; font-size: var(--fs-xs); letter-spacing: -.01em; color: var(--ink); }
.rbt-cell p:last-child { margin: 0; font-size: var(--fs-3xs); line-height: 1.45; color: var(--ink-3); }

/* --- the outputs and the two ends of the column --------------------------- */
.rbt__crm, .rbt__llm { display: grid; align-content: center; }

/* --- the wires ------------------------------------------------------------ */
.rbt__gap, .rbt__vgap { position: relative; }
.rbt__vgap { min-height: var(--vgap); }

.rbt-link { display: block; overflow: visible; }
/* A horizontal wire takes its width from the gap and its height from its own
   ratio, then sits in the middle of the cell — the boxes either side are
   centred in theirs, so the three middles line up. */
.rbt-link--h { width: 100%; height: auto; }
/* A horizontal wire and its label are a stack with a real gap between them,
   not a label floated over the arrow: whatever the label wraps to, the space
   below the wire is the same. */
.rbt__gap {
  display: grid; align-content: center; justify-items: center;
  gap: var(--s-3); padding: var(--s-3) 0;
}
/* The drops hang under the portal box rather than in the middle of the
   column: the portal is what is wired to the layer below, not the frame. */
.rbt-link--v {
  position: absolute; top: 0; bottom: 0;
  left: calc(var(--hang) - 24px); width: 48px; height: 100%;
}

.rbt-link__line, .rbt-link__head {
  fill: none; stroke: var(--c, var(--c-blue)); stroke-width: 1.1;
  stroke-linecap: round; stroke-linejoin: round;
  opacity: .5; vector-effect: non-scaling-stroke;
}
.rbt-link__head { opacity: .85; }

/* Cargo grows out of nothing at the start of its run and shrinks back into it
   at the end, so nothing pops in or out at the ends of a wire. The scale lives
   on the glyph and the motion on the <g> around it — one element cannot carry
   both a motion transform and a CSS one. `backwards` holds the 0% frame during
   a marker's animation-delay; without it, a marker that has not started yet
   shows at full size at the SVG's origin, which is the bright scrap in the
   corner. */
.rbt-carry > .rbt-dot,
.rbt-carry > .rbt-glyph {
  transform-box: fill-box; transform-origin: 50% 50%;
  animation: rbt-pop 2.8s linear infinite backwards;
}
.rbt-link--in .rbt-carry > * { animation-duration: 2.2s; }
.rbt-link--out .rbt-carry > *, .rbt-link--v .rbt-carry > * { animation-duration: 3s; }
.rbt-link--crm .rbt-carry:nth-of-type(2) > * { animation-delay: 1.4s; }
.rbt-link--v .rbt-carry:nth-of-type(2) > * { animation-delay: 1.5s; }
/* The two output wires run out of phase, or the envelope and the page leave
   the layer in lockstep and read as one event rather than two. The travel is
   animateMotion and this is a CSS animation, so the same offset is also given
   to the motion (OUT_BEGIN in tools/rubtur.py); delaying only one of the two
   leaves the glyph at its smallest half-way along the wire. */
.rbt__gap--t1 .rbt-carry > * { animation-delay: 1.5s; }

/* A quarter of the run to grow and a quarter to shrink: the growing is the
   part that has to be visible, or the marker reads as one that simply appears. */
@keyframes rbt-pop {
  0%   { transform: scale(0); opacity: 0; }
  26%  { transform: scale(1); opacity: 1; }
  74%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(0); opacity: 0; }
}

.rbt-dot {
  stroke: var(--c, var(--c-blue)); stroke-width: 5; stroke-linecap: round;
  filter: drop-shadow(0 0 5px currentColor); color: var(--c, var(--c-blue));
  opacity: .95;
}
.rbt-glyph {
  stroke: var(--c, var(--c-blue)); stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 4px currentColor); color: var(--c, var(--c-blue));
}

/* --- what each wire carries, in words ------------------------------------- */
.rbt-wlab {
  max-width: 100%; text-align: center; line-height: 1.45;
  font-family: var(--font-mono); font-size: var(--fs-3xs);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3);
  pointer-events: none;
}
/* The CRM wire's label is the longest on the figure. It no longer needs a
   measure of its own: the gap column is wide enough for its longest word, and
   the stack above puts it under the arrow rather than over it. */
.rbt-wlab--wide { width: auto; max-width: 100%; }

/* A drop is narrow, so its label sits beside the wire rather than over it. */
/* The horizontal cells stack their label under the wire, so the base rule is
   static; a drop's label has to be placed beside its own wire, so these three
   position themselves. */
.rbt-wlab--v, .rbt-wlab--v-in, .rbt-wlab--v-out { position: absolute; }
.rbt-wlab--v {
  left: calc(var(--hang) + 32px); translate: 0 -50%;
  bottom: auto; top: 50%; text-align: left;
}
/* The two directions, split apart: the request goes down the left of the pair
   with its words to its left, the answer comes up the right with its words to
   its right, so neither label can be read against the wrong arrow. */
.rbt-link--v-in  { left: calc(var(--hang) - 46px); }
.rbt-link--v-out { left: calc(var(--hang) + 2px); }
/* A fixed measure, not a percentage of the column. The outgoing label sits
   to the right of a hang point that is already three-quarters across, so 40%
   of the column left it about 130px — enough to wrap "получение данных с
   клиентского контура" onto four lines. 200px is two lines of this mono at
   this tracking, and the overflow goes into the gutter column, which is empty
   on this row. */
.rbt-wlab--v-in {
  right: calc(100% - var(--hang) + 54px); left: auto;
  bottom: auto; top: 50%; translate: 0 -50%; text-align: right;
  width: 200px; max-width: none;
}
.rbt-wlab--v-out {
  left: calc(var(--hang) + 62px); translate: 0 -50%;
  bottom: auto; top: 50%; text-align: left; width: 200px; max-width: none;
}

/* --- narrow: a layout, not a diagram -------------------------------------- */
/* Below the breakpoint there is no room for wires to run across, and arrows
   pointing sideways at a stacked list say nothing. Everything that moves goes
   away and what is left is the same information, in the same order, laid out
   to be read. */
@media (max-width: 1000px) {
  .rbt {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    gap: var(--s-3);
  }
  .rbt__contour, .rbt__crm, .rbt__hub, .rbt__tool, .rbt__llm {
    grid-area: auto; grid-column: 1;
  }
  .rbt__gap, .rbt__vgap { display: none; }
  .rbt__tool { padding: 0; }
  .rbt-zone { padding: var(--s-5) var(--s-3) var(--s-3); }
  .rbt-zone__row { grid-template-columns: minmax(0, 1fr); gap: var(--s-3); }
  .rbt-link--in { display: none; }
  /* The order is the whole argument, so the reading order gets a rail rather
     than losing the sequence with the arrows. */
  .rbt__crm, .rbt__tool, .rbt__llm {
    border-left: 2px solid var(--c, var(--c-blue));
  }
}
@media (max-width: 620px) {
  .rbt-hub__cells { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  .rbt-carry { display: none; }
}
