/* ============================================================================
   DOCSCROLL — web design system for the Vespa document factory
   Hues, faces and type ratios are taken verbatim from _build/brand/vespa.css.
   The print system is measured in pt on a fixed 1815x1251 canvas. This one is
   fluid: the same ramp, expressed in clamp() against the viewport, so a spread
   that was 62pt on paper is a display size on a phone and on a 27 inch screen.
   No new hues. Every colour here is a source token or a tint of one.
   ========================================================================= */

/* ------------------------------- faces ---------------------------------- */
@font-face{font-family:'DM Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/DMSans-400.woff2) format('woff2')}
@font-face{font-family:'DM Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/DMSans-500.woff2) format('woff2')}
@font-face{font-family:'DM Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/DMSans-600.woff2) format('woff2')}
@font-face{font-family:'DM Sans';font-style:normal;font-weight:700;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/DMSans-700.woff2) format('woff2')}
@font-face{font-family:'DM Sans Display';font-style:normal;font-weight:400;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/DMSansDisplay-400.woff2) format('woff2')}
@font-face{font-family:'DM Sans Display';font-style:normal;font-weight:500;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/DMSansDisplay-500.woff2) format('woff2')}
@font-face{font-family:'DM Sans Display';font-style:normal;font-weight:600;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/DMSansDisplay-600.woff2) format('woff2')}
@font-face{font-family:'Space Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/SpaceMono-400.woff2) format('woff2')}
@font-face{font-family:'Space Grotesk';font-style:normal;font-weight:500;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/SpaceGrotesk-500.woff2) format('woff2')}
@font-face{font-family:'Libre Baskerville';font-style:normal;font-weight:400;font-display:swap;src:url(/hubfs/RGP/vespa-travel-reader/fonts/LibreBaskerville-400.woff2) format('woff2')}

:root{
  /* ---- brand tokens, identical to print ---- */
  --ink:#2E2F27; --green:#61D790; --mist:#DAF0F7; --sage:#C4C5B9;
  --paper:#FFFFFF; --link:#4A86E8;

  --ink-72:rgba(46,47,39,.72); --ink-54:rgba(46,47,39,.54);
  --ink-38:rgba(46,47,39,.38); --ink-14:rgba(46,47,39,.14);
  --ink-07:rgba(46,47,39,.07); --ink-04:rgba(46,47,39,.04);
  --green-22:rgba(97,215,144,.22); --green-12:rgba(97,215,144,.12);
  --paper-72:rgba(255,255,255,.72);
  /* THE MUTED LIGHT TOKEN IS .54, and the name says so. It was `--paper-48`
     holding .54 for one round, because both document modules rendered
     `style="color:var(--paper-48)"` into the contents panel's paragraph and a
     rename would have rewritten `index.html` in two builds. That inline style
     is now the class `.toc__lede` below, so the name is free to be true.

     White on the two grounds this token actually lands on, measured by
     `text-contrast` rather than argued from the table. The panel is
     `color-mix(in srgb,var(--ink) 97%,transparent)` over the white act =
     rgb(52,53,45), and it is the WORSE of the two because it is the lighter:

                  panel rgb(52,53,45)   .act--ink rgb(46,47,39)
        .48            4.16:1                 4.37:1
        .53            4.71:1                 4.98:1
        .54            4.83:1                 5.11:1     <- lowest >= 4.8
        .55            4.95:1                 5.24:1
        .72            7.26:1                 7.80:1   (--paper-72, unchanged)
       1.00           12.36:1                13.53:1   (--paper, the row title)

     .54 is the lowest hundredth that clears 4.8:1 on the worse ground, which
     is the same rule the dossiers' label fix used. It stays a clear step below
     `--paper-72` at 7.26:1 and two steps below the white `.toc__t` title at
     12.36:1, so the panel's hierarchy is unchanged: title, then body, then
     number. */
  --paper-54:rgba(255,255,255,.54);
  --paper-14:rgba(255,255,255,.14); --paper-07:rgba(255,255,255,.07);

  /* THE MUTED ROLE ON A LIGHT GROUND, AND WHY IT IS NOT `--ink-54`.
     `--ink-54` is a print token: `_build/brand/vespa.css` declares it, the
     convergence figure carries `style="fill:var(--ink-54)"` on all twelve of
     its mono labels straight out of the print spread, and `extract-figure.py`
     copies that markup rather than re-drawing it. So its VALUE is not ours to
     move -- but .54 does not clear 4.5:1 on any ground this document has.
     `text-contrast` measured 52 runs under floor on the retrieval reader and
     24 on the managers reader, and this token was the colour of 43 and 17 of
     them: every eyebrow, every stat label and overline, `.split__cont`,
     `.bullets .n`, the ladder's rows, `.rung__n`, `.mono.muted`, and the
     figure's twelve labels.

     Ink at alpha a, composited on the three light grounds, same arithmetic the
     harness uses:

              white          mist rgb(218,240,247)   sage rgb(196,197,185)
        .54    3.27:1              3.10:1                  2.71:1
        .65    4.46:1              4.16:1                  3.46:1
        .70    5.19:1              4.78:1                  3.88:1
        .71    5.35:1              4.92:1                  3.97:1   <- taken
        .72    5.52:1              5.06:1                  4.07:1
       1.00   13.53:1             11.46:1                  7.75:1

     MIST IS THE WORST GROUND THIS ROLE LANDS ON, not white, so it sets the
     number: .71 is the lowest hundredth clearing 4.8:1 there. Sage clears
     nothing at any alpha below ~.79 and is handled where it belongs, on
     `.act--sage` below, by the same rule the link colour already uses.

     THE COST IS THE STEP TO `--ink-72`, and it is real: muted is now one
     hundredth off soft, so on white and mist the two are the same colour and
     the mute/soft distinction is carried by size, family and case alone, which
     is where the eyebrow, the stat label and the ladder row already carried
     most of it. A wider step would mean moving `--ink-72` too, which is the
     colour of the document's body prose, and that is a type decision rather
     than a contrast one. */
  --ink-mute:rgba(46,47,39,.71);

  /* THE GREEN THAT CAN BE TEXT ON A LIGHT GROUND. `--green` is the mint and it
     is 1.81:1 on white, which no alpha can fix: every tint of it is lighter.
     It shipped as the colour of the four `.pillar__num` ordinals, the seven
     lit ladder numerals and, on the managers reader, the five `.req__title`
     ordinals -- eleven and five runs that a reader cannot read at all.
        #61D790 (--green)  1.81:1 on white, 1.53:1 on mist, 7.49:1 on ink
        #2E7D4F            5.05:1 on white, 4.27:1 on mist, 2.68:1 on ink
     #2E7D4F is not a new hue: it is the fill both dossiers' CTA already uses,
     and white on it measures the same 5.05:1 from the other side. It clears
     4.8:1 on white as it stands, so nothing was darkened further; the ordinals
     it serves are large text with a 3:1 floor and it clears that by 68%.
     THE MINT IS UNCHANGED everywhere it was already right: rules, fills,
     borders, the rail's progress bar, and text on the ink ground at 7.49:1. */
  --green-ink:#2E7D4F;

  /* ---- ground, set per section, read by everything ---- */
  --ground:var(--paper);
  --on-ground:var(--ink);
  --on-ground-soft:var(--ink-72);
  --on-ground-mute:var(--ink-mute);
  --hair:var(--ink-14);

  --sans:'DM Sans',system-ui,sans-serif;
  --sans-display:'DM Sans Display','DM Sans',system-ui,sans-serif;
  --serif:'Libre Baskerville',Georgia,serif;
  --mono:'Space Mono',ui-monospace,monospace;
  --grot:'Space Grotesk','DM Sans',sans-serif;

  /* ---- fluid ramp. Ratios are the print ramp's, the anchor is the viewport.
         print 62/52/38/32/28/25/20/18/16/14/12 pt on a 1815pt canvas ---- */
  --t-display:clamp(2.75rem, 1.4rem + 5.2vw, 6.5rem);   /* 62pt */
  --t-title:  clamp(2rem, 1.35rem + 1.9vw, 3.5rem);     /* 52pt. Capped to the
     width of the 5fr column it sets in: 13 characters of DM Sans Display at
     3.5rem is 395px, and the column is 401px. Above that every long section
     title broke to three lines and left an orphan. */
  --t-h2:     clamp(1.75rem, 1.2rem + 2.4vw, 3.25rem);  /* 38pt */
  --t-section:clamp(1.5rem, 1.1rem + 1.7vw, 2.5rem);    /* 32pt */
  --t-deck:   clamp(1.25rem, 1rem + 1.15vw, 1.9rem);    /* 28pt */
  --t-serif:  clamp(1.15rem, .98rem + .8vw, 1.6rem);    /* 25pt */
  --t-body:   clamp(1.0625rem, .99rem + .38vw, 1.3rem); /* 20pt */
  --t-small:  clamp(.95rem, .9rem + .28vw, 1.15rem);    /* 18pt */
  --t-label:  clamp(.875rem, .85rem + .16vw, 1rem);     /* 16pt */
  --t-mono:   clamp(.72rem, .7rem + .13vw, .85rem);     /* 14pt */
  --t-micro:  clamp(.66rem, .65rem + .08vw, .74rem);    /* 12pt */

  /* ---- rhythm. One unit, everything is a multiple ---- */
  --u:clamp(.5rem, .4rem + .5vw, .875rem);
  --gap:calc(var(--u) * 2);
  --pad-x:clamp(1.25rem, 5vw, 7.5rem);
  --measure:34rem;              /* ~72ch at body size, the print 78ch ceiling */
  --shelf:clamp(4rem, 9vh, 8rem);/* vertical air between blocks */
  --chamfer:clamp(14px, 1.6vw, 26px); /* the 45 degree cut from the V mark */
  /* ---- one content width, and everything on the page uses it ----
     Chosen so the 7fr column of a .split lands ON the measure instead of
     stopping short of it. At 98rem the text column was 758px wide holding 544px
     of prose, so every paragraph ended 214px shy of its own cell, lined up with
     nothing, and the slack was a different size in each section. The grid and
     the measure have to be the same decision or neither one holds.
     The cover and the pinned figure use it too. They were full-bleed at first,
     which put their left edge 168px outboard of every section title, so the page
     visibly stepped inwards as you left the cover and stepped back out at the
     figure. Bleeding artwork past this edge is fine. Bleeding the type is not. */
  --max:64rem;
  --max-fig:84rem;   /* the pinned figure only: aligns left with --max, bleeds right */
}

*,*::before,*::after{box-sizing:border-box}
html{
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
  /* the rail offsets deep links so a heading does not land under the header */
  scroll-padding-top:6rem;
}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  margin:0;
  background:var(--ink);
  color:var(--ink);
  font-family:var(--sans);
  font-size:var(--t-body);
  line-height:1.6;
  font-weight:400;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

img,svg{max-width:100%;height:auto;display:block}
a{color:inherit}
h1,h2,h3,h4{margin:0;font-weight:500;text-wrap:balance}
/* The reset has to name every element whose UA margin would move it off the
   grid, not just the obvious ones. Each of these was silently indented:
   hr carries margin-inline:auto, which CENTRED every green rule in its column
   instead of aligning it left; blockquote and figure carry margin:1em 40px, so
   the pull quote sat 40px inboard of the paragraph above it; and ol carries
   padding-left:40px, which pushed the whole step ladder off the figure's edge.
   None of it is visible in the stylesheet, because the offending values are not
   written there. */
p,figure,blockquote,dl,dd,ol,ul,hr{margin:0}
ul,ol{padding:0;list-style:none}
hr{border:0}

/* =============================== grounds ================================ */
/* A ground is a section-level colour context. Print assigns one per spread and
   that assignment is the document's colour score, so it is inherited whole. */
.act{
  background:var(--ground);
  color:var(--on-ground);
  padding:var(--shelf) var(--pad-x);
  position:relative;
}
.act--paper{--ground:var(--paper)}
.act--mist{--ground:var(--mist)}
/* THE SAGE GROUND TAKES INK FOR EVERY ROLE, NOT ONLY FOR LINKS. rgb(196,197,185)
   is the darkest ground this document sets that is still a light ground, and
   the token table under `--ink-mute` is the whole argument: at .71 ink is
   3.97:1 there and at .72 it is 4.07:1, so neither the muted nor the soft role
   can be a tint on sage. The v6 link fix reached the same conclusion for
   `--link` and fixed the one selector a row had pointed at; `text-contrast`
   then found the rest of the family -- the two eyebrows, the summary's
   `.small.soft` CTA blurb, and at 390 the six `.deck` paragraphs of the
   workload act, which pass on desktop only because 30.4px type is graded on
   the 3:1 large-text floor and fail at 20.5px on a phone.
   So the ROLE TOKENS are redefined here rather than a list of classes being
   named: a selector list is a claim about every class it does not mention, and
   this ground has already been caught out by one. Hierarchy on sage is carried
   by size, family and case; there is no second readable value of this hue. */
.act--sage{
  --ground:var(--sage);
  --on-ground-soft:var(--ink);
  --on-ground-mute:var(--ink);
}
.act--ink{
  --ground:var(--ink);
  --on-ground:var(--paper);
  --on-ground-soft:var(--paper-72);
  --on-ground-mute:var(--paper-54);
  --hair:var(--paper-14);
}
.act__inner{max-width:var(--max);margin-inline:auto;width:100%}

/* =============================== type =================================== */
.display{font-family:var(--sans-display);font-size:var(--t-display);line-height:1.03;letter-spacing:-.022em;font-weight:500}
.title{font-family:var(--sans-display);font-size:var(--t-title);line-height:1.07;letter-spacing:-.02em;font-weight:500}
.h2{font-family:var(--sans-display);font-size:var(--t-h2);line-height:1.14;letter-spacing:-.015em;font-weight:500}
.section-title{font-family:var(--sans-display);font-size:var(--t-section);line-height:1.22;letter-spacing:-.012em;font-weight:500}
.deck{font-size:var(--t-deck);line-height:1.42;color:var(--on-ground-soft);letter-spacing:-.008em}
.small{font-size:var(--t-small);line-height:1.55}
.label{font-size:var(--t-label);line-height:1.4;font-weight:500}
.mono{font-family:var(--mono);font-size:var(--t-mono);letter-spacing:.04em}
.muted{color:var(--on-ground-mute)}
.soft{color:var(--on-ground-soft)}
.measure{max-width:var(--measure)}
.eyebrow{
  font-family:var(--mono);font-size:var(--t-mono);
  letter-spacing:.14em;text-transform:uppercase;color:var(--on-ground-mute);
}

.prose p + p{margin-top:1.1em}
.prose strong{font-weight:600}
.prose a,.link{
  color:var(--link);text-decoration:underline;
  text-decoration-thickness:1px;text-underline-offset:.18em;
}
.act--ink .prose a,.act--ink .link{color:var(--green)}
/* THE SAGE GROUND TAKES INK, AND THE NUMBER IS WHY. The summary act is
   rgb(196,197,185). Every token this sheet defines, composited on that ground:
     --ink 7.75:1   --ink-72 4.07:1   --link 2.05:1   --ink-38 1.95:1
     --paper 1.75:1  --mist 1.48:1  --green 1.03:1  (the rest are lower)
   Only --ink clears 4.5:1, and it is also the highest. The rule above used to
   carry `.act--sage .prose a` and NOT `.act--sage .link`, so the summary act's
   one CTA fell through to the root `--link` #4A86E8 and shipped at 2.05:1 from
   v1 of both readers. The half that WAS covered was worse: green on sage is
   1.03:1, invisible, which is what any contract paragraph that ever grows a
   link would have rendered as. Both halves are the same defect -- the sage
   ground has no light link colour -- so both are fixed here rather than the one
   a row happened to point at. The underline from `.prose a,.link` above is
   untouched, so ink still reads as a link and not as body copy. */
.act--sage .prose a,.act--sage .link{color:var(--ink)}

/* =============================== devices ================================ */
.rule{height:1px;background:var(--hair);border:0;width:100%}
.rule--green{height:3px;background:var(--green);width:clamp(2.5rem,4vw,3.75rem);border:0}

.chamfer{
  /* the 45 degree corner cut that echoes the V mark. Print cuts bottom-right. */
  clip-path:polygon(0 0,100% 0,100% calc(100% - var(--chamfer)),calc(100% - var(--chamfer)) 100%,0 100%);
}

.card{
  padding:calc(var(--u)*2.5) calc(var(--u)*3);
  background:var(--ink-04);
}
.card--green{background:var(--green-12)}
.card--mist{background:var(--mist)}
.act--ink .card{background:var(--paper-07)}

.plate{
  /* the full-width statement device: print's .callout on an ink ground */
  background:var(--ink);color:var(--paper);
  padding:clamp(1.5rem,3.5vw,3rem) clamp(1.5rem,4vw,3.5rem);
}
.plate .h2{color:var(--paper)}

.pull{
  border-left:3px solid var(--green);
  padding-left:calc(var(--u)*2.5);
  font-size:var(--t-serif);line-height:1.5;
  font-family:var(--serif);
}

.ic{width:1.55em;height:1.55em;color:currentColor;flex:none}

/* -------- the eight-capability tile grid (print .fig__grid) -------------- */
.fig{background:var(--mist);padding:calc(var(--u)*2.5)}
.fig__head{
  display:flex;gap:var(--gap);justify-content:space-between;align-items:flex-start;
  padding:0 calc(var(--u)*1.5) calc(var(--u)*2);
}
/* Was an inline max-width. A measure belongs in the stylesheet with the other
   measures, or it drifts from them the moment either one changes. */
.fig__note{max-width:32rem}
/* Eight cells, so the column count has to divide eight. auto-fit gave five at
   1400px and left the second row two empty white slots, which reads as a
   missing item rather than as a grid. Print sets 4x2 and print is right. */
.fig__grid{display:grid;gap:1px;background:var(--ink-14);grid-template-columns:1fr}
@media (min-width:38rem){.fig__grid{grid-template-columns:repeat(2,1fr)}}
@media (min-width:72rem){.fig__grid{grid-template-columns:repeat(4,1fr)}}
.fig__cell{
  background:var(--paper);
  padding:calc(var(--u)*2);
  display:flex;flex-direction:column;gap:calc(var(--u)*1.5);
  min-height:8.5rem;
}
.fig__cell .ic{color:var(--ink)}
.fig__cell p{font-size:var(--t-small);line-height:1.45}

/* --------------------------------- stats -------------------------------- */
/* A grid, not a wrapping flex row. At 64rem the four figures overflowed onto a
   second line with one item on it, which reads as a mistake rather than as a
   row of statistics. */
/* Two by two. Four figures in one row gave each a quarter of the content width,
   and "500 million" is 11 characters at display size wanting about 330px where
   a quarter is 236px, so it broke across two lines. A 2x2 gives each figure
   ~490px, holds the lead on one line, and reads as a block of numbers rather
   than as a strip. align-items:end bottom-aligns each row, so the two values in
   a row share a baseline even though one is set larger. */
.stats{
  display:grid;align-items:end;
  gap:calc(var(--u)*4) clamp(1.5rem,3vw,3rem);
  grid-template-columns:repeat(2,1fr);
}
.stat{min-width:0}
.stat__value{font-family:var(--sans-display);font-size:var(--t-h2);line-height:1;letter-spacing:-.02em;font-weight:500;font-variant-numeric:tabular-nums}
/* No large variant. In a four-across strip the lead figure earned a size of
   its own; in a 2x2 the grid already carries the structure, and 3.5rem beside
   3.25rem is an 8 percent difference that reads as an accident rather than as
   hierarchy. Four equal peers. */
.stat__label{font-family:var(--mono);font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;color:var(--on-ground-mute);margin-top:.5em}
.stat__over{font-family:var(--mono);font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;color:var(--on-ground-mute);margin-bottom:.55em}

/* ============================ layout blocks ============================= */
/* ONE spine. Every two-column block on the page divides at the same place, so
   the second column always starts at the same x and the page has a single
   vertical line running down it.
   There used to be three variants (5/7, even, wide-left) plus .pillar at 4/8,
   which put four different column starts on one page. Each one looked fine
   alone. In sequence the spine visibly wobbled, and the pillars' prose began
   108px left of every other paragraph in the document. */
.split{
  display:grid;gap:clamp(1.75rem,4vw,4.5rem);
  grid-template-columns:1fr;
  /* Both columns start on the SAME FIRST BASELINE, not merely at the same top
     edge. Aligned as boxes, a mono eyebrow beside a 1.9rem deck put the right
     column's first line 21px above the left's, and beside 1.3rem prose 10px
     below it, so the offset was different in almost every section. Grid
     baseline alignment resolves it from the type metrics instead of from a
     number someone has to keep re-guessing. */
  align-items:baseline;
}
/* Replaced elements and padded boxes have no useful first baseline, so they opt
   out and align to the top instead. */
.split > .figure,.split > .card,.split > .plate{align-self:start}
@media (min-width:64rem){
  .split{grid-template-columns:minmax(0,5fr) minmax(0,7fr)}
}
/* An empty spine slot is deliberate whitespace, not a missing block. It is what
   keeps a continued argument aligned with the argument it continues. */
.split__cont{
  font-family:var(--mono);font-size:var(--t-mono);letter-spacing:.1em;
  text-transform:uppercase;color:var(--on-ground-mute);
}
.stack{display:flex;flex-direction:column;gap:calc(var(--u)*2)}
.stack--l{gap:calc(var(--u)*4)}
/* Sticky only where there is a second column for the sticky thing to sit beside.
   Below the split's breakpoint the layout is one column, so a sticky head
   parks itself on top of the prose that scrolls under it: the title and the
   body text overlap and both become unreadable. Sticky is a two-column device
   and it belongs behind the two-column media query. */
.sticky-head{position:static}
@media (min-width:64rem){
  /* align-self is left to the split's baseline rule: a sticky grid item is
     sized to its content either way, so it still has the whole row to travel
     through. Forcing align-self:start here is what re-introduced the offset. */
  .sticky-head{position:sticky;top:clamp(4.5rem,12vh,7rem)}
}

/* ------------------------------ act heading ----------------------------- */

/* --------------------------- bullets, ruled ----------------------------- */
.bullets{display:flex;flex-direction:column}
.bullets li{
  border-top:1px solid var(--hair);
  padding:calc(var(--u)*2) 0;
  display:grid;gap:var(--gap);
  grid-template-columns:2.5rem 1fr;
  align-items:start;
}
.bullets li:last-child{border-bottom:1px solid var(--hair)}
.bullets .n{font-family:var(--mono);font-size:var(--t-mono);color:var(--on-ground-mute);padding-top:.35em}

/* ------------------------------- pillars -------------------------------- */
/* A pillar is a .split with a number instead of a title, so it divides in the
   same place and uses the same gap. It did not, and that was the single most
   visible misalignment on the page. */
.pillar{display:grid;gap:clamp(1.75rem,4vw,4.5rem);grid-template-columns:1fr}
@media (min-width:64rem){.pillar{grid-template-columns:minmax(0,5fr) minmax(0,7fr)}}
/* --green-ink, not --green: these are TEXT on the paper ground, at 1.81:1 in
   mint. See the token. */
.pillar__num{
  font-family:var(--sans-display);font-weight:500;
  font-size:clamp(3.25rem,7vw,7rem);line-height:.8;
  letter-spacing:-.04em;color:var(--green-ink);
}
.pillar__title{font-family:var(--sans-display);font-size:var(--t-h2);line-height:1.12;letter-spacing:-.015em;font-weight:500;margin-top:calc(var(--u)*1.5)}
.pillar__deck{font-size:var(--t-deck);line-height:1.4;color:var(--on-ground-soft);margin-top:calc(var(--u)*1.5)}

/* -------------------------------- cases --------------------------------- */
.cases{display:grid;gap:var(--gap);grid-template-columns:1fr}
@media (min-width:52rem){.cases{grid-template-columns:repeat(3,1fr)}}
.case__plate{
  background:var(--mist);
  height:clamp(5.5rem,8vw,7rem);
  display:flex;align-items:center;
  padding:0 calc(var(--u)*2.5);
}
.case__plate img{max-height:2.25rem;width:auto}
.case__claim{font-weight:600;font-size:var(--t-body);line-height:1.4;margin-top:calc(var(--u)*2.5)}
.case__body{font-size:var(--t-small);line-height:1.55;color:var(--on-ground-soft);margin-top:calc(var(--u)*1.5)}

/* ============================== the rail ================================ */
/* Print has a Contents spread. A web document does not need a page of links,
   it needs the contents present at all times, so spread 02 becomes this. */
.rail{
  position:fixed;z-index:40;top:0;left:0;right:0;
  padding:calc(var(--u)*1.5) var(--pad-x);
  background:color-mix(in srgb,var(--ink) 92%,transparent);
  color:var(--paper-72);
  backdrop-filter:blur(14px) saturate(140%);
  border-bottom:1px solid var(--paper-14);
  transform:translateY(-101%);
  transition:transform .42s cubic-bezier(.22,1,.36,1);
}
.rail.is-on{transform:none}
/* The rail carries the same inner container as the page, so its mark sits on
   the document's own left edge. Padded to --pad-x alone it sat 168px left of
   the content it labels once the content became a centred column. */
.rail__inner{
  max-width:var(--max);margin-inline:auto;width:100%;
  display:flex;align-items:center;gap:var(--gap);
}
.rail__mark{height:1.5rem;width:auto;flex:none}
.rail__now{
  font-family:var(--mono);font-size:var(--t-mono);letter-spacing:.06em;
  color:var(--paper);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* EVERY name the rail can show is in the markup, one <span> each, and doc.js
   toggles `hidden` instead of writing text. HUBSPOT-LEARNINGS 2.3: the script
   reveals, offsets, counts and pins, it never creates text, which is also what
   keeps the section names in the response body an AI crawler reads.
   NO `display` DECLARATION HERE ON PURPOSE. Any display value at this
   specificity outranks the UA stylesheet's `[hidden]{display:none}` and would
   paint all thirteen names at once. `font` and `color` inherit anyway; they are
   declared so the stand-in span can never introduce type or colour of its own. */
.rail__now-i{font:inherit;color:inherit}
.rail__spacer{flex:1 1 auto}
.rail__toc{
  border:0;background:transparent;color:var(--paper-72);cursor:pointer;
  font-family:var(--mono);font-size:var(--t-mono);letter-spacing:.1em;
  text-transform:uppercase;padding:.5rem;display:flex;gap:.6rem;align-items:center;
}
.rail__toc:hover{color:var(--paper)}
.rail__bar{
  position:absolute;left:0;bottom:-1px;height:2px;background:var(--green);
  width:var(--progress,0%);
}

/* the contents panel the rail opens: the real spread-02 index, on demand */
.toc{
  position:fixed;inset:0;z-index:50;
  background:color-mix(in srgb,var(--ink) 97%,transparent);
  color:var(--paper);
  padding:clamp(4.5rem,10vh,7rem) var(--pad-x) var(--shelf);
  overflow-y:auto;
  opacity:0;visibility:hidden;
  transition:opacity .3s ease,visibility .3s;
}
.toc.is-on{opacity:1;visibility:visible}
.toc__grid{max-width:var(--max);margin-inline:auto}
/* TWO COLUMNS AND THREE CHILDREN IS A COLUMN, A COLUMN AND A QUEUE.
   Below 64rem this was `2.5rem 1fr` with three children in it, so auto
   placement put the third one -- the blurb -- in row 2 of the FIRST track, the
   2.5rem one. Measured on the managers reader at 390, panel open: every one of
   eleven `.toc__b` boxes was exactly 40px wide beside a 293px title, so
   "The assessment" set as two 46px-tall lines of one word each, and "In
   practice" the same, inside a 350px panel with 250px of empty column beside
   them. Nothing in the file said 40px; it is the number column's own width,
   which is what auto placement does with a child nobody gave a column to.
   The number spans both rows instead, so the title and the blurb share the
   second column and the blurb reads as a line: 40px -> 293px, one line box
   each, and the rows got SHORTER (108/160/189px -> 96/125/154px) because
   nothing wraps eight deep any more. Above 64rem the three-column rule below
   still applies and is unchanged; `grid-row` is reset there because a number
   spanning two rows of a single-row grid would stretch that row. */
.toc__row{
  display:grid;gap:calc(var(--u)*.6) var(--gap);align-items:start;
  grid-template-columns:2.5rem minmax(0,1fr);
  padding:calc(var(--u)*2.25) 0;
  border-top:1px solid var(--paper-14);
  color:inherit;text-decoration:none;
}
.toc__row > .toc__n{grid-row:span 2}
@media (min-width:64rem){
  .toc__row{grid-template-columns:3rem minmax(0,5fr) minmax(0,6fr);gap:var(--gap)}
  .toc__row > .toc__n{grid-row:auto}
}
.toc__row:last-child{border-bottom:1px solid var(--paper-14)}
.toc__row:hover .toc__t{color:var(--green)}
.toc__n{font-family:var(--mono);font-size:var(--t-mono);color:var(--paper-54);padding-top:.5em}
.toc__t{font-family:var(--sans-display);font-size:var(--t-section);line-height:1.2;font-weight:500;transition:color .18s}
.toc__b{font-size:var(--t-small);line-height:1.5;color:var(--paper-54)}
/* The panel's standfirst, beside its heading. This was
   `style="color:var(--paper-48)"` rendered inline by both document modules,
   which is the only reason that token carried a name one round out of date:
   see it above. A class instead, so the colour is a stylesheet decision and
   `index.html` carries markup rather than paint. */
.toc__lede{color:var(--paper-54)}
.toc__close{
  /* Lands on the content's own right edge once the container is capped, rather
     than on the viewport margin, which is a different x. */
  position:absolute;top:calc(var(--u)*1.5);
  right:max(var(--pad-x), calc(50% - var(--max) / 2));
  background:transparent;border:0;color:var(--paper-72);cursor:pointer;
  font-family:var(--mono);font-size:var(--t-mono);letter-spacing:.1em;
  text-transform:uppercase;padding:.75rem;
}

/* ============================== the cover =============================== */
.cover{
  min-height:100svh;display:flex;flex-direction:column;justify-content:space-between;
  padding:var(--shelf) var(--pad-x) var(--shelf);
  position:relative;overflow:clip;
}
/* Every band of the cover sits in the page's own container, so the wordmark, the
   h1 and the colophon share the left edge of every section below them. The mark
   is positioned against .cover instead and still bleeds off the viewport. */
.cover > *:not(.cover__mark){max-width:var(--max);margin-inline:auto;width:100%}
.cover__mark{
  position:absolute;right:calc(var(--pad-x) * -.35);top:50%;
  width:min(46vw,38rem);translate:0 -50%;
  opacity:.98;pointer-events:none;
}
@media (max-width:63.99rem){
  .cover__mark{width:min(74vw,26rem);right:-14%;top:34%;opacity:.28}
}
.cover__body{position:relative;z-index:1}
.cover__foot{
  display:flex;justify-content:space-between;gap:var(--gap);
  font-family:var(--mono);font-size:var(--t-micro);letter-spacing:.06em;
  color:var(--paper-54);position:relative;z-index:1;
}
/* SIZED BY RENDERED HEIGHT, NOT BY WIDTH. HUBSPOT-LEARNINGS 3.4: the official
   Vespa.ai lockup is 5.0906:1 against the retired wordmark's 3.8496:1, so a
   file swap that keeps the WIDTH renders the mark 24% shorter, and height is
   what a reader reads as logo size. The three numbers below are the old width
   ramp divided by the old aspect (7/3.8496, 12/3.8496, 11/3.8496), so the
   rendered HEIGHT is unchanged at every viewport width and the two clamp
   breakpoints stay where they were. Measured: 44.89px at 1440, 29.09px at 390,
   before and after. */
.wordmark{height:clamp(1.8184rem,3.1172vw,2.8574rem);width:auto}
.cover .display{color:var(--paper)}

/* ============================ the summary photo ========================= */
.figure{overflow:clip;background:var(--ink-07);margin:0;align-self:start}
.figure img{width:100%;display:block}

/* ============================== the colophon ============================ */
.colophon{
  display:flex;flex-wrap:wrap;gap:var(--gap) clamp(1.5rem,4vw,3rem);
  justify-content:space-between;align-items:baseline;
  border-top:1px solid var(--hair);padding-top:calc(var(--u)*2);
  font-family:var(--mono);font-size:var(--t-micro);letter-spacing:.06em;
  color:var(--on-ground-mute);
}
.socials{display:flex;flex-wrap:wrap;gap:calc(var(--u)*2)}
.socials a{
  font-family:var(--mono);font-size:var(--t-mono);letter-spacing:.08em;
  text-transform:uppercase;text-decoration:none;color:var(--paper-72);
  border-bottom:1px solid var(--paper-14);padding-bottom:.35em;
}
.socials a:hover{color:var(--green);border-color:var(--green)}

/* ============================== the companion =========================== */
/* The link to the sibling edition, in the close of both readers. It lived in
   the managers reader's `modes.css` and the retrieval reader borrowed
   `.small soft` + `.section-title` to approximate it, so one component wore two
   sets of clothes. It is a SHARED pattern -- both readers carry it, both put it
   in the same slot -- so it belongs in the file both readers share, and moving
   it here is what makes them the same component rather than two that look
   alike. Declarations are byte-identical to the modes.css originals.

   `.companion__l` is its own class, NOT `.eyebrow`, even though it is
   declaration-for-declaration what `.eyebrow` is. `align.mjs` measures the gap
   from an act's FIRST `.eyebrow` to the title that follows it, and in the close
   the title comes first, so an eyebrow down here reported -592.9px of drift.
   The audit was right: `.eyebrow` means "the label above a section title" and
   this label is not one. */
.companion{
  margin:0;
  font-family:var(--sans-display);
  font-size:var(--t-section);
  line-height:1.2;
  font-weight:500;
}
.companion__l{
  font-family:var(--mono);font-size:var(--t-mono);
  letter-spacing:.14em;text-transform:uppercase;color:var(--on-ground-mute);
}

/* ====================== the pinned diagram (the peak) =================== */
/* One section, held for four viewport heights while the argument assembles.
   The figure is the source SVG unchanged. Only opacity and a dash offset are
   driven, so the artwork cannot be broken by the animation. */
.pin{position:relative}
.pin__track{height:var(--pin-track,320vh)}
.pin__stage{
  position:sticky;top:0;height:100svh;
  display:grid;grid-template-rows:minmax(0,1fr);align-items:center;
  /* No horizontal padding. The .act around it already supplies --pad-x, and
     adding it again here indented this section's content by exactly twice the
     page margin, so the one section built to be looked at was the only one not
     aligned with the rest of the document. */
  padding:calc(var(--u)*7) 0 calc(var(--u)*3);
}
/* Aligns left with the document, bleeds right.
   Confined to --max the figure rendered 573px wide, which scales its 11.5px
   mono labels down to 7.7px and makes the one object on the page built to be
   studied the only one that cannot be read. So the left margin is computed to
   land on the flow's own left edge and the figure takes whatever is left, which
   is the rule already stated in the tokens: bleeding artwork past this edge is
   fine, bleeding the type is not. */
.pin__grid{
  /* No width:100% here. Combined with the computed left margin it asked for the
     full parent width PLUS the offset and overflowed the section by exactly the
     offset. An auto-width block grid with one margin set fills what is left. */
  max-width:var(--max-fig);
  margin-left:max(0px, calc((100% - var(--max)) / 2));margin-right:0;
  display:grid;gap:clamp(1.75rem,4vw,4.5rem);grid-template-columns:1fr;
  align-items:center;height:100%;
}
/* 4/8 rather than the flow's 5/7, because the ladder is short labels and the
   figure is the thing being looked at. It is the one ratio on the page that
   differs, and it differs inside a container that shares the page's own edges,
   so the outer spine is unaffected. */
@media (min-width:64rem){.pin__grid{grid-template-columns:minmax(0,4fr) minmax(0,8fr)}}
/* The panel has to fit the stage, not merely be told a maximum. A fixed 76svh
   on the svg plus the panel's own padding and caption ran past the bottom of
   the viewport and clipped the fourth card off the figure.
   The fix needs BOTH halves. Constraining the panel alone (max-height plus
   flex-basis:0) made the figure collapse to nothing instead: flex-basis 0 means
   the svg starts at zero height and grows only into free space, and a panel
   with no definite height of its own has none to give. So the panel is
   stretched to a definite height first, and the artwork takes what is left. */
.pin__fig{
  display:flex;flex-direction:column;
  align-self:stretch;height:100%;min-height:0;
}
.pin__fig svg{flex:1 1 auto;min-height:0;width:100%;height:auto}
/* THE FIGURE'S TWELVE LABELS ARE RE-THEMED BY TOKEN, NOT BY OVERRIDE.
   Each one carries `style="fill:var(--ink-54)"` from the print spread, copied
   verbatim by `extract-figure.py`, which does not touch a coordinate or an
   attribute of an approved drawing. An inline `fill` cannot be beaten by a
   stylesheet rule without `!important`, and custom properties INHERIT, so the
   print token is re-pointed for this subtree and the inline declaration
   resolves to the measured muted value on its own. It survives the figure
   being regenerated, which an edited SVG would not, and it leaves `--ink-54`
   itself telling the truth about its own alpha everywhere else.
   Measured before: twelve mono labels at 3.27:1 on the white act. After: 5.35:1. */
.pin__fig svg{--ink-54:var(--ink-mute)}
/* The step ladder. Seven rows, one per group of the figure, the live one lit.
   It exists because a figure that assembles needs an index: without it the
   reader can see something is happening but not what the sequence is. */
.ladder{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--hair)}
.ladder li{
  display:grid;grid-template-columns:2.25rem 1fr;gap:var(--gap);
  align-items:baseline;padding:calc(var(--u)*1.15) 0;
  border-bottom:1px solid var(--hair);
  color:var(--on-ground-mute);
  transition:color .35s ease,opacity .35s ease;
}
.ladder li span:first-child{font-family:var(--mono);font-size:var(--t-micro)}
.ladder li span:last-child{font-size:var(--t-small);line-height:1.3}
.ladder li[data-on="1"]{color:var(--on-ground)}
.ladder li[data-live="1"]{color:var(--on-ground)}
/* The lit step's numeral is the one place the ladder uses the accent as type,
   and the ladder sits on the paper ground: --green-ink, for the same reason
   `.pillar__num` does. */
.ladder li[data-live="1"] span:first-child{color:var(--green-ink)}
.ladder li[data-live="1"] span:last-child{font-weight:600}

.pin__count{
  font-family:var(--mono);font-size:var(--t-mono);letter-spacing:.12em;
  color:var(--on-ground-mute);
}
/* The pinned figure's caption, same rule as .rail__now-i above and for the same
   reason: all eight captions (the figure's own, then one per step) are in the
   markup and doc.js toggles `hidden`. No `display` here, or `[hidden]` loses. */
.pin__cap{font:inherit;color:inherit}
@media (max-width:63.99rem){
  /* No pinning on a phone. The figure is tall, the viewport is not, and a
     pinned 877-unit-tall SVG at phone width is unreadable. It scrolls. */
  .pin__track{height:auto}
  .pin__stage{position:static;height:auto;padding:0}
}

/* ============================ the case rail ============================= */
/* Lateral travel reads as "options" where vertical reads as "argument", so the
   three customers pan sideways. Native scroll-snap, not a JS carousel: it
   keeps keyboard, trackpad and touch behaviour for free. */
@media (max-width:51.99rem){
  .cases{
    display:flex;overflow-x:auto;scroll-snap-type:x mandatory;
    gap:var(--gap);margin-inline:calc(var(--pad-x) * -1);
    padding:0 var(--pad-x) var(--gap);
    scrollbar-width:none;
  }
  .cases::-webkit-scrollbar{display:none}
  .cases > li{flex:0 0 min(78vw,20rem);scroll-snap-align:start}
}

/* ==================== the rail advertises its travel ==================== */
/* MOVED HERE FROM THE PORT on 2026-09-18. This was a PORT ADDITION in
   `hubspot/targets.py` (RAIL_AFFORDANCE), which meant the LOCAL build shipped
   the defect and only the hosted copy was fixed. The declarations below are
   byte-identical to the block the port was appending; only this comment is
   new. HUBSPOT-LEARNINGS 3.1 and PLAYBOOK's `rail-affordance` row.

   MEASURED PROBLEM (390x844): the case rail above is a native scroll-snap flex
   row with the scrollbar deliberately hidden. Card 1 is fully visible, card 2
   shows 23%, card 3 shows 0%, and 596px of content sits off-screen with
   NOTHING indicating it. A 23% slice of a card reads as a clipped card, not as
   "there is more to the right".

   FIX: the classic pure-CSS scroll shadow. THE ATTACHMENT IS THE WHOLE TRICK.
   The ground-coloured cover patches are `local` so they scroll away with the
   content; the shadows are `scroll` so they stay pinned to the box. At the left
   end the cover hides the left shadow; scroll right and it slides off and
   reveals it. Each edge therefore advertises travel ONLY in the direction that
   has any left, which a static right-edge fade does not: that one would still
   be fading the third card once the reader reached it. `var(--ground)` inherits
   whatever ground the rail sits on, so this works on paper, mist, sage and
   ink without a second rule. */
@media (max-width:51.99rem){
  .cases{
    background-image:
      linear-gradient(to right, var(--ground), transparent),
      linear-gradient(to left,  var(--ground), transparent),
      linear-gradient(to right, var(--ink-14), transparent),
      linear-gradient(to left,  var(--ink-14), transparent);
    background-position:left center,right center,left center,right center;
    background-size:2.5rem 100%,2.5rem 100%,1.25rem 100%,1.25rem 100%;
    background-repeat:no-repeat;
    background-attachment:local,local,scroll,scroll;
  }
  /* Where a platform does paint a scrollbar, show a slim one rather than
     hiding it. On iOS this is inert, which is why the shadow above carries
     the affordance on its own. */
  .cases{scrollbar-width:thin;scrollbar-color:var(--ink-14) transparent}
  .cases::-webkit-scrollbar{display:block;height:3px}
  .cases::-webkit-scrollbar-thumb{background:var(--ink-14);border-radius:2px}
  .cases::-webkit-scrollbar-track{background:transparent}
}

/* ========================== motion, opt-in only ========================= */
/* Nothing here is required to read the document. Every reveal target is fully
   visible by default; the .js class on <html> is what hides it, so a crawler,
   a reader with JS off, and a printer all get the finished page. */
.js [data-reveal]{opacity:0;transform:translateY(1.35rem)}
.js [data-reveal].is-in{
  opacity:1;transform:none;
  transition:opacity .72s cubic-bezier(.22,1,.36,1),transform .72s cubic-bezier(.22,1,.36,1);
  transition-delay:var(--d,0ms);
}
/* The wipe clips a CHILD, never the observed element itself.
   clip-path reduces an element's intersection rectangle to nothing, so an
   IntersectionObserver reports ratio 0 for a target it has clipped and the
   reveal never fires: the hidden state hides the element from the thing meant
   to un-hide it. Three ink plates shipped invisible before this was measured.
   opacity and transform do not have this problem, which is why every other
   device here uses them. */
.js [data-wipe] > *{clip-path:inset(0 100% 0 0)}
.js [data-wipe].is-in > *{
  clip-path:inset(0 0 0 0);
  transition:clip-path 1.05s cubic-bezier(.22,1,.36,1);
}
.js [data-draw]{transform:scaleX(0);transform-origin:left}
.js [data-draw].is-in{transform:none;transition:transform .8s cubic-bezier(.22,1,.36,1);transition-delay:var(--d,0ms)}

@media (prefers-reduced-motion:reduce){
  .js [data-reveal],.js [data-wipe] > *,.js [data-draw]{
    opacity:1 !important;transform:none !important;clip-path:none !important;transition:none !important;
  }
  .pin__track{height:auto !important}
  .pin__stage{position:static !important;height:auto !important}
  .rail{transition:none}
}

/* ================================ print ================================= */
@media print{
  .rail,.toc{display:none !important}
  .act{padding:1.5cm;break-inside:avoid}
  .pin__track{height:auto}
  .pin__stage{position:static;height:auto;padding:0}
  [data-reveal],[data-wipe] > *,[data-draw]{opacity:1 !important;transform:none !important;clip-path:none !important}
}

/* ============================ focus, always ============================= */
:where(a,button,[tabindex]):focus-visible{
  outline:2px solid var(--green);outline-offset:3px;border-radius:2px;
}
.skip{
  position:absolute;left:-9999px;top:0;background:var(--green);color:var(--ink);
  padding:.75rem 1.25rem;z-index:100;font-weight:600;
}
.skip:focus{left:var(--pad-x)}
