/* ==========================================================================
   Research story supplement — loads on top of customers-v2.css.

   customers-v2.css is installed verbatim from the design project and is not edited,
   so everything a technical write-up needs that a customer story does not lives here:
   code blocks, the wide attack-chain diagram, a data table, and one callout line.

   Zero new colours and zero new type sizes: the site already ships a brand monospace
   ("YTF millie mono", the face behind buttons, .kicker and .blog_tag-wrapper), so code
   is set in it rather than a system stack.
   ========================================================================== */

/* --- accent phrases inside quotes -----------------------------------------------

   customers-v2.css styles .customer_feature-accent (the hero quote) but not the
   quote or closing variants — those live in customers-supplement.css, which this page
   does not load, because that file also carries the customer story's video styles and
   its Webflow-timeline overrides. Ported rather than pulled in wholesale, so the two
   pages cannot drift into each other's fixes.

   Italic correction, same as the supplement: an italic leans forward, so its opening
   glyph overhangs backwards into the preceding word space. Leading side only —
   trailing padding visibly detaches the comma or full stop that follows.            */

.customer_quote-accent,
.customer_closing-accent {
  color: var(--text--accent);
  font-style: italic;
}

.customer_quote-accent,
.customer_closing-accent,
.customer_feature-accent { padding-inline-start: .09em; }

/* --- the hero: title, standfirst, byline, illustration --------------------------

   Straight down the page in reading order. One title, nothing beside it competing for
   the first look, and the illustration below the byline where a news feature puts it.

   The h1 is the page's one big type moment, so it runs larger than customers-v2.css's
   hero title and on a headline measure rather than the body's.                     */

/* Title and standfirst run the full container. customers-v2.css caps the hero title at
   46rem and the sub at 40rem, which is right for a story that shares its row with a
   facts column — here nothing sits beside them, so the caps only leave the top of the
   page looking half-used. */

/* Pi's .navbar is position: fixed (98px, 103px below 991px), so the first thing in the
   hero has to clear it itself. customers-v2.css hangs that clearance off
   .customer_eyebrow; this page leads with the breadcrumb instead, so the clearance
   moves onto the block — otherwise the title renders underneath the nav. */

.rsr_headline {
  max-width: none;
  margin: 0;
  padding-top: calc(var(--customer-nav-clear) + 2.25rem);
}

/* The headline is set in YTF Millie Bold, not the Giong serif customers-v2.css uses.
   Giong ships weight 400 only — Roman and Italic, no bold file — so a heavier serif
   headline would be browser-synthesised faux bold, which smears the stems and looks
   worse at 56px than it does at 16. Millie ships a real 700, and Millie is already the
   heading face on this page (.customer_h2 runs it at 500), so this is the same voice
   one step louder rather than a new one. */

.rsr_headline .customer_hero-title {
  font-family: "YTF millie", sans-serif;
  /* 500 Medium, not 700 Bold. Millie ships 300/400/500/700 as real files, so this is
     the genuine middle weight rather than a synthesised one — and 500 is what
     .customer_h2 already uses, so the headline and the section headings are one family
     at two sizes instead of two different voices. */
  font-weight: 500;
  /* Fluid, because a fixed 56px killed the responsive step the h2 token used to
     provide and left the headline running to ten lines on a phone. Floor is the
     token's own mobile value (34px), ceiling 56px. */
  font-size: clamp(2.125rem, 5.2vw, 3.5rem);
  line-height: 1.1;           /* bold sans sets tighter than the serif did */
  letter-spacing: -.015em;    /* medium needs less pulling in than bold did */
  max-width: none;
  margin-top: .75rem;
}

.rsr_headline .customer_hero-sub { max-width: none; }

.rsr_back { margin-bottom: .25rem; }

/* The illustration opens the article column, with the table of contents beside it.
   It is opaque on near-white, so it sits on a plate rather than floating on the
   background — the same plate the attack chain gets, so the two read as a set. Nearly
   square, so it is capped by height and centred; cropping it to a banner would cut the
   tower off. The cap is set so the plate finishes at roughly the height of the contents
   card next to it, and the two bottom out together. */

.rsr_hero-figure {
  margin: 0 0 3.5rem;
  padding: 1.5rem;
  display: flex;
  justify-content: center;
  background-color: var(--base-colors--snow-white);
  border: 1px solid var(--borders--primary);
  border-radius: var(--_sizes---border-radium--large, 1.25rem);
}

/* The illustration is 16:9, so WIDTH is the binding dimension and it fills the plate.
   The previous one was nearly square and had to be capped by height instead, which is why
   this was `width: auto; max-height: 22rem`: that cap on a wide image would have left it
   626px inside a 736px box, floating in white on both sides.

   At full width this lands about 414px tall, and the plate closes at ~462px, which is
   within a few pixels of the contents card beside it. The two still bottom out together,
   which was the point of the cap in the first place. max-height stays as a guard so a
   portrait illustration cannot run away down the page. */

.rsr_hero-img {
  display: block;
  width: 100%;
  max-width: 100%;
  max-height: 30rem;
  height: auto;
  object-fit: contain;
}

/* --- the attack-chain diagram ------------------------------------------------- */

/* The figure lives in the article column, which is 784px against a 1120px container —
   tight for five labelled steps. It borrows the 5rem gutter and stops exactly at the
   rail's leading edge. It cannot take the rail's width too: the rail holds the table of
   contents and is sticky, so it is on screen for the whole article and paints above
   static content. A figure crossing into it is covered by the TOC, not framed by it. */

/* Aligned to the article column, not broken out of it.

   This used to be calc(100% + 5rem): 864px in a 784px column, taking the whole gutter so
   the figure stopped exactly at the rail's leading edge with zero gap. That was borrowed
   width for a diagram that had to scroll, and it left the picture touching the contents
   card. Both figures now fit at their natural size, so the extra 80px buys nothing and
   costs the gutter. At column width every block in the article lines up. */

.rsr_diagram {
  margin: 2.5rem 0;
  width: 100%;
  max-width: none;
}

/* The figure is a button: the whole picture opens the lightbox, and it is one keyboard
   stop that announces itself, rather than a div with a click handler bolted on. */
.rsr_diagram-scroll {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--borders--primary);
  border-radius: var(--_sizes---border-radium--large, 1.25rem);
  background-color: var(--backgrounds--secondary);
  cursor: zoom-in;
  -webkit-appearance: none;
  appearance: none;
  transition: border-color .2s ease, box-shadow .2s ease;
}

.rsr_diagram-scroll:hover {
  border-color: color-mix(in srgb, var(--base-colors--navy) 22%, transparent);
  box-shadow: 0 6px 22px -12px color-mix(in srgb, var(--base-colors--navy) 45%, transparent);
}

.rsr_diagram-scroll:focus-visible {
  outline: 2px solid var(--base-colors--gem-teal);
  outline-offset: 3px;
}

/* The affordance. Always present rather than hover-only, because on a touch screen there
   is no hover and an invisible control is no control. It just gets more contrast on
   hover and focus. */
.rsr_zoom {
  position: absolute;
  top: .75rem;
  right: .75rem;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: .625rem;
  color: var(--base-colors--navy);
  background-color: color-mix(in srgb, var(--base-colors--snow-white) 82%, transparent);
  border: 1px solid var(--borders--primary);
  backdrop-filter: blur(6px);
  transition: background-color .2s ease, color .2s ease, transform .2s ease;
}

.rsr_zoom svg { width: 1rem; height: 1rem; display: block; }

.rsr_diagram-scroll:hover .rsr_zoom,
.rsr_diagram-scroll:focus-visible .rsr_zoom {
  background-color: var(--base-colors--navy);
  color: var(--base-colors--snow-white);
  transform: scale(1.06);
}

/* --- the lightbox ---------------------------------------------------------------

   Built on <dialog> and opened with showModal(), so focus trapping, Escape to close,
   inertness of the page behind it and top-layer stacking all come from the platform
   instead of from JavaScript that would get them subtly wrong.                     */

.rsr_lightbox {
  width: 100vw;
  height: 100dvh;
  max-width: 100vw;
  max-height: 100dvh;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: hidden;
}

.rsr_lightbox::backdrop {
  background-color: color-mix(in srgb, var(--base-colors--navy) 82%, transparent);
  backdrop-filter: blur(8px);
}

/* The backdrop is the click target for dismissing, so the figure sits inside a grid that
   fills the dialog and the image stops the click from reaching it. */
.rsr_lightbox-stage {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  padding: clamp(1rem, 4vw, 3rem);
  cursor: zoom-out;
}

.rsr_lightbox-img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  cursor: default;
  border-radius: var(--_sizes---border-radium--large, 1.25rem);
  background-color: var(--base-colors--snow-white);
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, .55);
}

/* Wide-image mode, set by research.js when fitting to width would waste the screen.

   A 3:1 diagram fitted to a portrait phone renders about 358x117: larger than nothing and
   useless, because the labels are the reason anyone tapped it. Here the image is sized by
   HEIGHT instead and the stage pans horizontally, which is how you actually read a wide
   diagram on a narrow screen. JS decides, because the threshold depends on the image's
   aspect against the viewport's and CSS cannot see the first one. */

.rsr_lightbox.is-wide .rsr_lightbox-stage {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  place-items: center start;
  cursor: grab;
  scrollbar-width: thin;
}

.rsr_lightbox.is-wide .rsr_lightbox-img {
  max-width: none;
  max-height: none;
  width: auto;
  height: 100%;
}

.rsr_lightbox-close {
  position: absolute;
  top: clamp(.75rem, 2vw, 1.5rem);
  right: clamp(.75rem, 2vw, 1.5rem);
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--base-colors--snow-white) 30%, transparent);
  background-color: color-mix(in srgb, var(--base-colors--snow-white) 12%, transparent);
  color: var(--base-colors--snow-white);
  cursor: pointer;
  transition: background-color .2s ease, transform .2s ease;
}

.rsr_lightbox-close:hover { background-color: color-mix(in srgb, var(--base-colors--snow-white) 26%, transparent); transform: scale(1.06); }
.rsr_lightbox-close:focus-visible { outline: 2px solid var(--base-colors--gem-teal); outline-offset: 3px; }
.rsr_lightbox-close svg { width: 1.125rem; height: 1.125rem; display: block; }

/* Open and close animate; a dialog cannot transition from display:none without
   allow-discrete, so the states are declared explicitly. */
@media (prefers-reduced-motion: no-preference) {
  .rsr_lightbox,
  .rsr_lightbox::backdrop {
    transition: opacity .24s ease, overlay .24s allow-discrete, display .24s allow-discrete;
    opacity: 0;
  }
  .rsr_lightbox[open],
  .rsr_lightbox[open]::backdrop { opacity: 1; }
  @starting-style {
    .rsr_lightbox[open],
    .rsr_lightbox[open]::backdrop { opacity: 0; }
  }
  .rsr_lightbox-img { transition: transform .26s cubic-bezier(.2, .8, .3, 1); }
  .rsr_lightbox[open] .rsr_lightbox-img { transform: scale(1); }
  @starting-style {
    .rsr_lightbox[open] .rsr_lightbox-img { transform: scale(.96); }
  }
}

/* The image fits its figure and never forces a scroll.

   It used to carry min-width: 60rem, a floor chosen to keep the step labels readable,
   which meant the figure scrolled at every width including desktop. That was the wrong
   trade: a diagram cropped at its right edge with no visible affordance reads as broken
   rather than as scrollable, and this one now opens the piece as a graphical abstract,
   where the job is to show the shape of the attack at a glance. The five steps are each
   walked through in prose below, so no label here has to carry the argument alone. */

.rsr_diagram-img {
  display: block;
  width: 100%;
  height: auto;
}

.rsr_diagram-cap {
  font-size: var(--_fonts---body--small);
  line-height: var(--_fonts---line-height--large);
  color: var(--text--primary);
  /* A caption measure, not the figure's — a single line stretched across the whole
     figure reads as a stray sentence rather than a caption. */
  max-width: 46rem;
  margin: 1rem auto 0;
  text-align: center;
  text-wrap: pretty;
}

/* --- code ----------------------------------------------------------------------

   The code plate is navy, not the near-white the rest of the page runs on, and that is
   a contrast decision rather than a stylistic one. Syntax colouring only works if the
   colours are the brand's, and measured against the light --base-colors--cold-grey the
   brand accents do not carry text: orange #ff7329 is 2.5:1 and gem-teal #00ccd3 is
   2.9:1, both far under the 4.5:1 that WCAG 2.1 SC 1.4.3 asks of 15px type. Against
   navy #001d3f the same two measure 6.2:1 and 8.5:1. So the plate inverts and the
   palette is used at full strength instead of being muddied into legibility.

   The code itself is NOT set in "YTF millie mono", even though that is the site's
   brand monospace and the obvious choice. That face draws U+0022 and U+0027 as
   typographic quotes: pure-ASCII source renders as value.split(“.”), and a reader
   copying it sees syntax that looks wrong. Verified in the browser against the same
   string with font-variant-ligatures:none and liga/calt/dlig/salt all off — the curl
   is in the glyph, not a feature, so there is nothing to switch off.

   That is a label face doing its job: everywhere the site uses it (buttons, .kicker,
   .blog_tag-wrapper, .toc_heading) the text is uppercase words with no punctuation.
   So the LABEL keeps the brand face, and only the code drops to a real mono.        */

.rsr_code {
  margin: 2rem 0;
  background-color: var(--base-colors--navy);
  border-radius: var(--_sizes---border-radium--large, 1.25rem);
  overflow: hidden;              /* clips the header bar and the full-bleed mark bands */

  /* Every token colour on the plate, declared once. Each is a palette member or a mix
     of two, so nothing here is a new brand colour, and each is checked against the navy
     ground: dim 5.4:1, key 6.2:1, str 8.5:1, name 10.3:1, num and fg above 12:1. */
  --rsr-code-fg:   color-mix(in srgb, var(--base-colors--sky) 72%, var(--base-colors--cold-mid-grey));
  --rsr-code-dim:  var(--base-colors--cold-mid-grey);
  --rsr-code-key:  var(--base-colors--orange);
  --rsr-code-str:  var(--base-colors--gem-teal);
  --rsr-code-name: var(--base-colors--frost);
  --rsr-code-num:  var(--base-colors--peach);
}

/* The label sits inside the plate, not above it. Above, on a page of near-white, a
   line of small grey caps between two paragraphs reads as stray text; inside, the
   header bar makes the block one object and gives the language somewhere to live. */

.rsr_code-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: .75rem 1.5rem;
  background-color: color-mix(in srgb, var(--base-colors--snow-white) 7%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--base-colors--snow-white) 10%, transparent);
}

.rsr_code-label,
.rsr_code-lang {
  font-family: "YTF millie mono", monospace;
  font-size: var(--_fonts---body--xxsmall);
  letter-spacing: var(--_fonts---letter-spacing--large);
  text-transform: uppercase;
  line-height: 1.45;
  margin: 0;
}

/* The label takes the brand accent as a marker, the same orange the pen and the stat
   arrows use, so the top of every block carries one piece of Pi in it. */
.rsr_code-label {
  color: var(--base-colors--sky);
  display: flex;
  align-items: center;
  gap: .5rem;
}

.rsr_code-label::before {
  content: "";
  flex: none;
  width: .375rem;
  height: .375rem;
  border-radius: 50%;
  background-color: var(--base-colors--orange);
}

/* The language reads as a tag, which is the idiom the site already uses for one on
   .blog_tag-wrapper: a pill, quiet, sitting at the end of a header row. */
.rsr_code-lang {
  color: var(--base-colors--frost);
  flex: none;
  padding: .1875rem .5rem;
  border-radius: 999px;
  background-color: color-mix(in srgb, var(--base-colors--frost) 14%, transparent);
}

.rsr_code pre {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;
  font-size: var(--_fonts---body--small);
  line-height: 1.7;              /* code is scanned line by line, so it leads wider */
  color: var(--rsr-code-fg);
  background: none;
  border: 0;
  border-radius: 0;
  padding: 1.25rem 0;
  margin: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  white-space: pre;
  tab-size: 2;
}

/* Webflow's base sheet sets `all: inherit` on code inside pre — restore the family
   the pre just declared, or the block silently renders in the body face.

   inline-block with min-width:100% is what lets a marked line's band reach the full
   scroll width. Without it the band stops at the viewport edge, so scrolling a long
   snippet right runs the highlight out from under the code it is highlighting. */
.rsr_code pre code {
  font: inherit;
  color: inherit;
  background: none;
  padding: 0;
  border: 0;
  display: inline-block;
  min-width: 100%;
}

.rsr_code-line {
  display: block;
  padding: 0 1.5rem;
  border-left: 2px solid transparent;
}

/* Two levels of emphasis, answering different questions. The band says "this is the
   part of the snippet that matters"; the pen says "these exact characters are the bug".
   A skimmer who reads neither still sees where to look. */

.rsr_code-line.is-mark {
  background-color: color-mix(in srgb, var(--base-colors--orange) 13%, transparent);
  border-left-color: var(--base-colors--orange);
}

.rsr_code-hit {
  background-color: var(--base-colors--orange);
  color: var(--base-colors--navy);      /* 6.2:1 on the orange */
  border-radius: .1875rem;
  padding: .1em .2em;
  margin: 0 -.05em;
}

/* The pen overrides the token colour underneath it, and it has to out-specify the token
   rules below to do it: those are `.rsr_code .o` (0,2,0), so a bare `.rsr_code-hit span`
   (0,1,1) loses and the run keeps its syntax colour on the orange ground. Measured at
   1.16:1 before this selector was qualified. */
.rsr_code .rsr_code-hit span { color: inherit; font-weight: 500; }

/* Redaction bars. The glyphs underneath are U+2588 full blocks, so the bar is solid
   before any styling; the rule only sets its colour and fills the hairline gaps between
   glyphs. Matching the plate exactly would make it vanish into the panel, so it sits a
   step lighter, which is also how the redactions read in the original captures. */
.rsr_code-redact {
  color: color-mix(in srgb, var(--base-colors--snow-white) 26%, transparent);
  text-shadow: 0 0 1px color-mix(in srgb, var(--base-colors--snow-white) 26%, transparent);
  border-radius: .125rem;
  user-select: none;
}

.rsr_code-note {
  font-size: var(--_fonts---body--xsmall);
  line-height: 1.55;
  color: var(--base-colors--cold-mid-grey);
  border-top: 1px solid color-mix(in srgb, var(--base-colors--snow-white) 10%, transparent);
  margin: 0;
  padding: .875rem 1.5rem;
  text-wrap: pretty;
}

/* --- syntax tokens (Pygments short names, emitted at build time) ----------------- */

.rsr_code .c, .rsr_code .c1, .rsr_code .cm, .rsr_code .cp, .rsr_code .cs,
.rsr_code .ch, .rsr_code .cpf { color: var(--rsr-code-dim); font-style: italic; }

.rsr_code .k, .rsr_code .kd, .rsr_code .kc, .rsr_code .kn, .rsr_code .kr,
.rsr_code .kt, .rsr_code .kp, .rsr_code .ow { color: var(--rsr-code-key); }

.rsr_code .s, .rsr_code .s1, .rsr_code .s2, .rsr_code .sb, .rsr_code .sc,
.rsr_code .sd, .rsr_code .se, .rsr_code .sh, .rsr_code .sr, .rsr_code .ss,
.rsr_code .sx, .rsr_code .l { color: var(--rsr-code-str); }

/* ${...} inside a template literal is not string content, it is an escape back into
   code, so it takes the keyword colour and the reader can see the seam. */
.rsr_code .si { color: var(--rsr-code-key); }

.rsr_code .nb, .rsr_code .ne, .rsr_code .nf, .rsr_code .nc, .rsr_code .nn,
.rsr_code .na, .rsr_code .nd { color: var(--rsr-code-name); }

.rsr_code .m, .rsr_code .mi, .rsr_code .mf, .rsr_code .mh, .rsr_code .mo,
.rsr_code .mb { color: var(--rsr-code-num); }

.rsr_code .o, .rsr_code .p { color: var(--rsr-code-dim); }
.rsr_code .n, .rsr_code .nx, .rsr_code .nv, .rsr_code .py { color: var(--rsr-code-fg); }
.rsr_code .err { color: var(--rsr-code-key); }

/* --- the rail: navigation first, facts second -----------------------------------

   A long technical post needs a reading plan more than it needs a fact sheet, so the
   table of contents takes the top of the rail and the facts sit under it. The rail is
   sticky (customers-v2.css), so it can outgrow the viewport — it scrolls itself rather
   than pushing content off screen.                                                  */

.rsr_side {
  max-height: calc(100vh - 11rem);
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: thin;
  padding-right: .25rem;
}

/* The contents sit in a card, the way the site's own blog TOC does (.toc-wrapper), so
   that beside the illustration's plate the two read as a matched pair rather than a
   picture next to loose text. */
.rsr_toc {
  display: block;
  background-color: var(--base-colors--snow-white);
  border: 1px solid var(--borders--primary);
  border-radius: var(--_sizes---border-radium--large, 1.25rem);
  padding: 1.5rem 1.25rem;
}

.rsr_side-facts {
  margin-top: 2rem;
  padding-top: 0;
}

.rsr_toc-heading {
  font-family: "YTF millie mono", monospace;
  font-size: var(--_fonts---body--xxsmall);
  letter-spacing: var(--_fonts---letter-spacing--large);
  text-transform: uppercase;
  color: var(--base-colors--cold-mid-grey);
  margin: 0 0 1rem;
}

.rsr_toc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: toc;
  /* The hairline is the spine every entry hangs off, the same device the timeline
     uses. The active entry replaces its segment with the accent. */
  border-left: 1px solid var(--borders--primary);
}

.rsr_toc-link {
  counter-increment: toc;
  display: block;
  padding: .4rem 0 .4rem 1rem;
  margin-left: -1px;
  border-left: 2px solid transparent;
  font-size: var(--_fonts---body--small);
  line-height: var(--_fonts---line-height--large);
  color: var(--base-colors--cold-mid-grey);
  text-decoration: none;
  text-wrap: pretty;
  transition: color .2s ease, border-color .2s ease;
}

.rsr_toc-link::before {
  content: counter(toc, decimal-leading-zero);
  font-family: "YTF millie mono", monospace;
  font-size: var(--_fonts---body--xxsmall);
  color: var(--borders--secondary);
  margin-right: .5rem;
  font-variant-numeric: tabular-nums;
  transition: color .2s ease;
}

.rsr_toc-link:hover { color: var(--text--secondary); }

.rsr_toc-link.is-current {
  color: var(--text--secondary);
  border-left-color: var(--text--accent);
}

.rsr_toc-link.is-current::before { color: var(--text--accent); }

/* --- byline ---------------------------------------------------------------------- */

.rsr_byline {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
  margin: 2rem 0 0;
}

/* Overlapping avatars, same idiom as the card on /blog. flex-direction: row-reverse with
   a negative margin puts each earlier author ON TOP of the next, so the first-listed author
   is the one fully visible rather than the last. */
.rsr_byline-avatars {
  display: flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  flex: none;
}

.rsr_byline-avatars .rsr_byline-img + .rsr_byline-img { margin-right: -.75rem; }

.rsr_byline-img {
  width: 2.5rem;
  height: 2.5rem;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid var(--base-colors--sky);
  object-fit: cover;
  flex: none;
}

.rsr_byline-who { display: flex; flex-direction: column; }

.rsr_byline-name {
  font-size: var(--_fonts---body--small);
  color: var(--text--secondary);
  line-height: 1.3;
}

.rsr_byline-role,
.rsr_byline-meta {
  font-family: "YTF millie mono", monospace;
  font-size: var(--_fonts---body--xxsmall);
  letter-spacing: var(--_fonts---letter-spacing--small);
  text-transform: uppercase;
  color: var(--base-colors--cold-mid-grey);
}

.rsr_byline-meta {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding-left: .75rem;
  margin-left: .25rem;
  border-left: 1px solid var(--borders--primary);
}

.rsr_byline-dot { color: var(--borders--secondary); }

/* Anchored headings should not land under the fixed 98px navbar. */
.customer_h2[id],
.rsr_summaries { scroll-margin-top: 8rem; }

/* --- inline links in body copy -------------------------------------------------- */

.rsr_link {
  color: var(--text--secondary);
  text-decoration: none;
  box-shadow: inset 0 -1px 0 0 var(--text--accent);
  transition: color .2s ease, box-shadow .2s ease;
}

.rsr_link:hover {
  color: var(--text--accent);
  box-shadow: inset 0 -2px 0 0 var(--text--accent);
}

/* --- inline code in body copy ---------------------------------------------------

   A technical token set in the body face has no edges: "the value @attacker.com#
   redirected AWS traffic" runs the payload straight into the sentence and the reader
   cannot see where the string stops. The capsule is the markdown backtick idea, built
   from tokens the site already owns: --base-colors--cold-grey is the same ground the
   code blocks used before they went navy, and --borders--primary is the card hairline.

   Sized in em, not rem, so it scales with whatever it sits in: 20px body copy, the 18px
   callout, the 15px stat caption. 0.875em because a monospace face runs optically larger
   than the body face at the same nominal size.

   Set in the real mono stack, not "YTF millie mono", for the reason the code blocks are:
   that face draws U+0022 and U+0027 as typographic quotes.                          */

.rsr_ic {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;
  font-size: .875em;
  line-height: 1;                 /* the capsule must not open up the line it sits on */
  color: var(--text--secondary);  /* full navy, 15.3:1 on the capsule ground */
  background-color: var(--base-colors--cold-grey);
  border: 1px solid var(--borders--primary);
  border-radius: .3125rem;
  padding: .15em .35em;
  /* A capsule broken across two lines reads as two capsules, so tokens stay whole. The
     longest in this post (AssumeRoleWithWebIdentity) is ~260px against a 350px column at
     390px wide, so nothing overflows. */
  white-space: nowrap;
}

/* A capsule inside the navy code plate (the notes carry tokens too) keeps the shape but
   takes the plate's own palette. The light grey version is built for a near-white ground
   and reads as a bright chip punched into the dark panel. */
.rsr_code .rsr_ic {
  color: var(--base-colors--sky);
  background-color: color-mix(in srgb, var(--base-colors--snow-white) 10%, transparent);
  border-color: color-mix(in srgb, var(--base-colors--snow-white) 20%, transparent);
}

/* --- callout lines -------------------------------------------------------------- */

.rsr_callout {
  font-family: "YTF millie", sans-serif;
  font-size: var(--_fonts---headings--h6);
  line-height: var(--_fonts---line-height--xmedium);
  font-weight: 500;
  color: var(--text--secondary);
  margin: 3rem 0;
  text-wrap: pretty;
}

/* --- the patched-SDK table ----------------------------------------------------- */

.rsr_table { margin: 2.25rem 0 0; }

.rsr_table-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border-top: 1px solid var(--borders--primary);
}

.rsr_table table {
  border-collapse: collapse;
  width: 100%;
  min-width: 36rem;
  font-variant-numeric: tabular-nums;
}

.rsr_table th {
  font-family: "YTF millie mono", monospace;
  font-size: var(--_fonts---body--xxsmall);
  letter-spacing: var(--_fonts---letter-spacing--large);
  text-transform: uppercase;
  font-weight: 400;
  color: var(--base-colors--cold-mid-grey);
  text-align: left;
  padding: 1rem .875rem 1rem 0;
  border-bottom: 1px solid var(--borders--primary);
  white-space: nowrap;
}

.rsr_table td {
  font-size: var(--_fonts---body--small);
  line-height: var(--_fonts---line-height--large);
  color: var(--text--primary);
  padding: .9375rem .875rem .9375rem 0;
  border-bottom: 1px solid var(--borders--primary);
  vertical-align: top;
}

.rsr_table tbody tr:last-child td { border-bottom: 0; }

/* The language column names the thing; the version column is the actionable one. */
.rsr_table td:first-child { color: var(--text--secondary); white-space: nowrap; }
.rsr_table td:nth-child(3),
.rsr_table td:nth-child(4) {
  font-family: "YTF millie mono", monospace;
  font-size: var(--_fonts---body--xsmall);
}
.rsr_table td:nth-child(4) { color: var(--text--secondary); }
.rsr_table td:last-child { white-space: nowrap; }

.rsr_table-foot {
  font-size: var(--_fonts---body--xsmall);
  line-height: var(--_fonts---line-height--large);
  color: var(--base-colors--cold-mid-grey);
  border-top: 1px solid var(--borders--primary);
  margin: 0;
  padding-top: 1rem;
  text-wrap: pretty;
}

/* --- related link in the rail --------------------------------------------------- */

.rsr_side-related { border-top: 1px solid var(--borders--primary); padding-top: 1.75rem; }

.rsr_side-link { display: block; text-decoration: none; }

.rsr_side-link-title {
  display: block;
  font-size: var(--_fonts---body--small);
  line-height: var(--_fonts---line-height--large);
  font-weight: 500;
  color: var(--text--secondary);
  text-wrap: pretty;
}

.rsr_side-link:hover .rsr_side-link-title { color: var(--text--accent); }

.rsr_side-link-text {
  display: block;
  font-size: var(--_fonts---body--xsmall);
  line-height: var(--_fonts---line-height--large);
  color: var(--text--primary);
  margin-top: .25rem;
  text-wrap: pretty;
}

/* --- the timeline's date stamps -------------------------------------------------

   customers-v2.css sizes the stamp for a m:ss clock. A date is roughly twice as wide,
   so at seven columns it needs to shed a little size and tighten its letter-spacing to
   stay on one line rather than wrapping unevenly from column to column.            */

.customer_timeline-time {
  font-size: var(--_fonts---body--xxsmall);
  letter-spacing: -.01em;
  white-space: nowrap;
}

/* Same accessibility fix as the labels further down, and the one that pass missed.
   customers-v2.css turns the date stamps `--base-colors--deep-teal` once the timeline
   resolves, which measures 2.89:1 at 11px against this ground and needs 4.5:1. These
   stamps are content, not decoration: they are the disclosure record.

   Darkened toward navy rather than replaced, so the resolved state still reads as the
   same teal signal, one step deeper. The mix ratio was chosen by measuring in the
   browser, not by arithmetic. */

.customer_timeline.is-resolved .customer_timeline-time {
  color: color-mix(in srgb, var(--base-colors--deep-teal) 70%, var(--base-colors--navy));
}

.customer_timeline-icon.is-count {
  font-size: .82rem;
  letter-spacing: -.01em;
}

/* .rsr_diagram-hint is gone: nothing emits it. The corner icon and the zoom-in cursor
   carry the affordance, and a caption under every figure repeating it was noise. */

@media screen and (max-width: 991px) {
  /* Back inside the column once the rail is no longer beside it. */
  .rsr_diagram { width: 100%; margin: 2rem 0; }

  /* customers-v2.css turns the rail into a static two-column band above the article at
     this width. The sticky scrollbox has to come off with it, or the page opens with a
     viewport-tall scrolling box before a word of the story. */
  .rsr_side { max-height: none; overflow-y: visible; padding-right: 0; display: block; }

  /* The card framing is for the side-by-side arrangement; stacked above the article it
     is just a box around the width of the page, so it comes off. */
  .rsr_toc { background: none; border: 0; border-radius: 0; padding: 0; }

  .rsr_toc-list {
    /* Eleven entries down a phone is most of a screen. Two columns keeps the reading
       plan glanceable, which is the only reason it is above the article at all. */
    columns: 2;
    column-gap: 1.5rem;
    border-left: 0;
    padding-left: 0;
  }
  .rsr_toc-link {
    break-inside: avoid;
    border-left: 0;
    padding-left: 0;
    margin-left: 0;
  }
  .rsr_toc-link.is-current { border-left: 0; }

  .rsr_side-facts {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.75rem 2rem;
    border-top: 1px solid var(--borders--primary);
    padding-top: 1.75rem;
  }

  /* Same move customers-v2.css makes on the rail: one column becomes two rows, and the
     facts sit under the headline as a pair of columns rather than a narrow rail. */
  .rsr_hero-grid { grid-template-columns: 1fr; gap: 2.5rem; }
  .rsr_hero-facts {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.75rem 2rem;
    padding-top: 0;
    border-top: 1px solid var(--borders--primary);
    padding-block-start: 2.25rem;
  }
  .rsr_hero-facts .customer_side-group + .customer_side-group { margin-top: 0; }
}

@media screen and (max-width: 767px) {
  /* 1.5rem of plate on each side costs a sixth of a 350px column, and the illustration is
     the thing worth looking at. The labels inside it are not legible at this width either
     way; they are texture here, and the alt text carries what they say. */
  .rsr_hero-figure { padding: .75rem; margin-bottom: 2.5rem; }

  .rsr_code pre { padding: 1rem 0; font-size: var(--_fonts---body--xsmall); }
  .rsr_code-line { padding: 0 1.125rem; }
  .rsr_code-head,
  .rsr_code-note { padding-left: 1.125rem; padding-right: 1.125rem; }
  .rsr_callout { margin: 2.5rem 0; }
  .rsr_side-related { border-top: 0; padding-top: 0; }
  /* The mobile timeline is a vertical list, so each stamp has the full width and can
     go back to the body size customers-v2.css intended. */
  .customer_timeline-time { font-size: var(--_fonts---body--xsmall); letter-spacing: 0; }
}

@media screen and (max-width: 479px) {
  /* Matches customers-v2.css, which drops the rail to one column at this width. */
  .rsr_hero-facts { grid-template-columns: 1fr; }
  .rsr_side-facts { grid-template-columns: 1fr; }
  .rsr_byline-meta { padding-left: 0; margin-left: 0; border-left: 0; flex-basis: 100%; }
}

/* ==========================================================================
   READING TYPOGRAPHY
   ==========================================================================

   The complaint was that the page reads dense. The cause is measurable: every
   reading-level element inherits --_fonts---line-height--large (1.4em), which is the
   top of the site's leading scale and is correct for the 30-60 character columns it
   was built for — nav, cards, the rail. In this article column the same 1.4 is doing
   87 characters, and in the standfirst 124. A 1.4 ratio at 87 characters is roughly
   what 1.4 at 50 feels like: the eye loses the line on the return sweep.

   So: leading grows with measure, and every vertical gap is derived from one unit
   rather than picked per element. --rsr-line is one line of body text; the whole
   article spaces on halves and multiples of it. New variable, not a redefined token,
   so nothing leaks into the nav or footer sharing this document.

   Contrast is NOT the cause and is not touched on body copy: --text--primary
   composites to rgb(76,96,120) here, 6.4:1, comfortably past AA.               */

:root { --rsr-line: 2rem; }            /* 32px — one line of body text */

/* --- headline: 1.08 is a display ratio applied to a three-line headline. At this
   size the lines were nearly touching, and the -0.75px tracking tightened the word
   spacing along with it. */

.rsr_headline .customer_hero-title {
  line-height: 1.15;                    /* 50 / 57.5 */
  letter-spacing: -.005em;              /* was -.015em */
}

/* --- standfirst: the page's worst offender at 124 characters on 1.4. Size up so the
   measure falls to ~101 characters, then lead it at 1.6. Larger also separates it
   from the 20px body below, so it reads as a distinct voice rather than a first
   paragraph that happens to be wide. */

.rsr_headline .customer_hero-sub {
  font-size: 1.375rem;                  /* 22px */
  line-height: 1.6;                     /* 35.2px */
}

/* --- body prose: the main event, 27 paragraphs of it. Same measure class, so the
   same treatment across all four selectors. 20px drops the measure from 87 to ~78
   characters and 1.6 leading is the standard for that measure. */

.customer_para,
.customer_summary-list li,
.customer_summary p:not(.customer_summary-label),
.customer_numbered-text {
  font-size: 1.25rem;                   /* 20px */
  line-height: var(--rsr-line);         /* 32px = 1.6 */
}

.customer_para { margin-bottom: var(--rsr-line); }
.customer_summary-list li + li { margin-top: var(--rsr-line); }
.customer_summary p:not(.customer_summary-label) + p { margin-top: var(--rsr-line); }

/* The paragraph gap was 24px against 25.2px of leading — a break between paragraphs
   narrower than the space between lines, which is why blocks of text ran together.
   It is now a full line step. */

/* Half-leading grew from 3.6px to 6px, so the bullet has to drop with it or it sits
   above the first line's optical centre. Derived, not eyeballed. */
.customer_summary-list li::before { top: .68em; }

/* --- section headings: leading is fine at 1.2; the fault was the gap. 80px above
   separates sections, 16px below binds a heading to its own text. */

/* A heading belongs to the text under it, so the space below has to be clearly smaller
   than the space between two paragraphs. It was exactly equal to it (32px, one full line),
   which left every heading floating between its section and the previous one instead of
   attaching to its own copy. 80 above, 20 below: the gap now says which side it is on. */

.customer_h2 {
  line-height: 1.25;
  margin: calc(var(--rsr-line) * 2.5) 0 calc(var(--rsr-line) * .625);
}

/* --- display quotes and callouts: 1.2 is tight for two- and three-line settings. */

.customer_quote-text { line-height: 1.35; }
.customer_quote { margin: calc(var(--rsr-line) * 2) 0; }
/* Same asymmetry, and for the same reason: each callout states a point that the next
   paragraph elaborates, so it leans forward. 64 above, 32 below, where it used to be 64
   on both sides and read as a floating slab. */
.rsr_callout { line-height: 1.35; margin: calc(var(--rsr-line) * 2) 0 var(--rsr-line); }
/* A callout straight after a heading would otherwise inherit the full section gap. */
.customer_h2 + .rsr_callout { margin-top: var(--rsr-line); }

/* --- block interruptions: code, figures, tables and lists were at four different
   near-miss values (32 / 40 / 36 / 44px). One tier, so the article has one answer. */

/* --- the stat band -------------------------------------------------------------

   Three problems, all measured rather than eyeballed:

   1. It had NO top margin, so its first hairline landed directly under the last line of
      "Why it matters" and the band read as part of that paragraph.
   2. The value track is a fixed 272px, but the widest value ("3.7 LOW -> takeover")
      needs 187px. Every row therefore carried ~85px of dead space, then a 32px gutter,
      so the caption began 384px in and the two halves of a row stopped looking related.
   3. The caption sat at the site's default 14px against a 28px serif value. Next to
      20px body copy directly above it, that reads as fine print rather than as the
      explanation of the number beside it.                                            */

.customer_stats { margin-top: calc(var(--rsr-line) * 1.5); }

.customer_stat-caption {
  font-size: var(--_fonts---body--medium);
  line-height: 1.6;
}

/* The two-column row is scoped to the width that actually has two columns.
   customers-v2.css collapses .customer_stat to a single column below 767px, but this
   sheet loads after it, so an unscoped override here wins at every width regardless of
   the media query on theirs: it pinned a 208px value column against a 136px caption on
   a phone. Matching their breakpoint hands mobile back to them. */

@media screen and (min-width: 768px) {
  .customer_stat {
    /* Just past what the widest value needs, so the caption starts beside its number
       instead of across a gulf. 1fr for the caption gives it a ~540px measure. */
    grid-template-columns: 13rem 1fr;
    align-items: baseline;
    padding-top: var(--rsr-line);
    padding-bottom: var(--rsr-line);
  }
}

.rsr_code,
.rsr_diagram,
.rsr_table,
.customer_changes,
.customer_numbered {
  margin-top: calc(var(--rsr-line) * 1.5);
  margin-bottom: calc(var(--rsr-line) * 1.5);
}

.rsr_diagram-cap { font-size: var(--_fonts---body--medium); line-height: 1.55; }
.rsr_table td { line-height: 1.55; padding-top: 1.125rem; padding-bottom: 1.125rem; }
.customer_changes li { font-size: var(--_fonts---body--medium); line-height: 1.6; }

/* --- rail and contents: 14px on 1.4 across a 210px column, with 32px rows. Larger
   rows are also a better pointer target. */

.rsr_toc-link {
  font-size: var(--_fonts---body--medium);   /* 15px */
  line-height: 1.5;
  padding: .5rem 0 .5rem 1rem;
}

.customer_side-value { line-height: 1.5; }
.customer_side-note { line-height: 1.5; margin-top: .375rem; }
.customer_side-list li { line-height: 1.5; }

.rsr_byline { margin-top: calc(var(--rsr-line) * 1.5); }

/* --- ACCESSIBILITY FIX, not a taste call.
   --base-colors--cold-mid-grey (#8992a0) measures 3.12:1 against this page's ground.
   Every use below is 11-12px, which needs 4.5:1 under WCAG 2.1 SC 1.4.3 — the 3:1
   allowance only applies at 24px, or 18.66px bold. --text--primary is 6.4:1 at the
   same weight and reads as the same quiet grey. Measured in-browser, not estimated. */

/* .rsr_code-label and .rsr_code-lang are deliberately absent: they sit on the navy code
   plate, not on the page ground, so they are measured against navy instead and carry
   their own colours (sky 14.6:1, cold-mid-grey 5.4:1). Recolouring them to
   --text--primary here would set navy-on-navy. */

.rsr_toc-heading,
.customer_side-label,
.rsr_table th,
.rsr_table-foot,
.customer_timeline-caption,
.customer_timeline-foot,
.rsr_byline-role,
.rsr_byline-meta,
/* The contents entries and the back link are the same #8992a0 at 15px and 14px, i.e. the
   same 3.12:1, and both are navigation rather than ornament. The first pass fixed the
   contents HEADING and left the links under it, which is the wrong half. */
.rsr_toc-link,
.rsr_back,
/* The back link's label is a child div, and the site's own .button.is-text rule colours
   that div directly, so colouring only the anchor leaves the visible text untouched. */
.rsr_back div {
  color: var(--text--primary);
}

/* Which leaves the current entry needing to out-rank its neighbours by more than colour,
   since they are now 6.4:1 and it is 15:1. It already carries the accent rule and the
   accent numeral; this makes the label itself heavier so the three cues agree. */
.rsr_toc-link.is-current { font-weight: 500; }

/* Decorative separator, aria-hidden, so SC 1.4.3 does not apply. It was
   --borders--secondary at 1.32:1, which is not "quiet", it is invisible: the byline read
   as "24 AUGUST 2026  6 MIN READ" with an unexplained gap. Quieter than the text it
   separates, but actually present. */
.rsr_byline-dot { color: var(--base-colors--cold-mid-grey); }

/* Those same labels set none of their own leading, so they inherit a line-height that
   was resolved in em against a 16px parent — 22.4px on 11px type, a ratio of 2.04. On a
   one-line uppercase label that reads as a gap, not as leading. Pre-existing; fixed here
   because a typography pass that measured it and left it would be a pass that did not
   look. */

.rsr_toc-heading,
.customer_side-label,
.rsr_table th,
.rsr_byline-role,
.rsr_byline-meta { line-height: 1.45; }

/* --- responsive: the measure collapses, so the unit and the sizes come back down.
   At 390px the body column is ~350px — 42 characters — where 20px/1.6 would be far
   too open and the type too large for the width. */

@media screen and (max-width: 991px) {
  .rsr_headline .customer_hero-sub { font-size: 1.25rem; line-height: 1.6; }
}

@media screen and (max-width: 767px) {
  :root { --rsr-line: 1.75rem; }        /* 28px */

  .customer_para,
  .customer_summary-list li,
  .customer_summary p:not(.customer_summary-label),
  .customer_numbered-text {
    font-size: var(--_fonts---body--large);   /* 18px */
    line-height: var(--rsr-line);             /* 28px = 1.56 */
  }

  .rsr_headline .customer_hero-title { line-height: 1.18; letter-spacing: 0; }
  .rsr_headline .customer_hero-sub { font-size: 1.125rem; }
  .customer_h2 { margin: calc(var(--rsr-line) * 2) 0 calc(var(--rsr-line) * .625); }
  .customer_summary-list li::before { top: .62em; }
}

/* Emphasis inside the standfirst. YTF Millie ships real 300/400/500/700 files, so 700
   is a genuine Bold rather than a synthesised one. The colour step matters as much as
   the weight: the dek sits at --text--primary (navy at 70%), so lifting the phrase to
   full navy is what makes it read as emphasis at a glance. */

.customer_hero-sub strong {
  font-weight: 700;
  color: var(--text--secondary);
}

/* Same treatment wherever **bold** is used in body copy. The colour step matters as much
   as the weight: this copy sits at --text--primary (navy at 70%), so lifting the phrase to
   full navy is what makes it read as emphasis in a skim. */

.customer_para strong,
.customer_stat-caption strong,
.customer_summary-list li strong,
.customer_summary p strong,
.customer_changes li strong,
.rsr_callout strong {
  font-weight: 700;
  color: var(--text--secondary);
}
