/* ==========================================================================
   GROWACK — EDITORIAL FIGURE SYSTEM
   The shared visual language for every /resources/ page (blogs + events).

   WHY THIS FILE EXISTS
   26 resource pages carry real argument and, until now, no bespoke
   argument-carrying diagram. This file is the single place the diagram
   vocabulary lives, so those 26 pages cannot drift apart from each other.

   THE BAR — THE SHAPE TEST
   The geometry alone must carry the argument. Strip every label off a figure
   built here and a reader should still get the point. Anything that fails
   that test is decoration and does not belong in an article.

   THE TRUTH RULE (non-negotiable)
   A figure may only show what the article itself states. No invented number,
   no invented stage, no invented relationship. When a diagram shows shape
   rather than measured data, its caption says "Schematic" out loud.

   PALETTE — fixed, nothing else is permitted
     bg      #F8FAFC   surface #FFFFFF / #F1F5F9   border #E2E8F0
     ink     #111827   muted   #626977 (AA-safe stand-in for #6B7280)
     orange  #F97316 fill / #C2410C ink
     teal    #0F766E / #E0F2F1
     cream   #FFF7ED -> #FEF3C7
   Tints are produced with fill-opacity on an allowed hue only. No new hues.

   BUILD RULES
     - inline SVG or CSS geometry only; never a raster chart, never a library
     - every figure readable at 390px and never a cause of horizontal scroll
     - full-width diagrams: HTML layout + SVG/CSS marks, so label text never
       scales below its nominal size
     - panel diagrams (<= ~340px wide): a single self-contained inline SVG,
       because at that width the viewBox scale stays at or above 1.0
     - text alternative on the graphic, decorative marks aria-hidden
     - no motion; nothing here depends on JS
   ========================================================================== */

:root {
  --fig-cream: #fff7ed;          /* the lighter end of the cream ramp */
  --fig-cream-deep: #fef3c7;     /* = --color-primary-soft, named for figures */
  --fig-rule: #e2e8f0;           /* every hairline and every bar track */
  /* was a hard-coded 8px, the fattest rail on the site and 2px above every other
     bar idiom. Reads the house bar token so the figure rails move with the rest. */
  --fig-bar-h: var(--bar-h, 6px);
  --fig-notch: 13px;             /* chevron depth in .figflow */
}

/* ==========================================================================
   0. LAYOUT REPAIRS THE FIGURE SYSTEM DEPENDS ON
   A figure cannot be "readable at 390px" if its column has already blown out.
   `grid-template-columns: 1fr` resolves to minmax(AUTO, 1fr), so one long
   unbreakable token anywhere in the column (a UTM string, a raw URL) sets the
   column's minimum to that token's width and the whole page scrolls sideways.
   Fixed at the system level so no individual page has to remember.
   ========================================================================== */
.article-layout { grid-template-columns: 260px minmax(0, 1fr); }
@media (max-width: 940px) {
  .article-layout { grid-template-columns: minmax(0, 1fr); }
}
.article-body,
.article-nav { min-width: 0; }
.article-body p,
.article-body li,
.article-body h2,
.article-body h3,
.article-body h4,
.article-nav a { overflow-wrap: break-word; }
/* a bare URL or tracking string is the one case that needs mid-token breaking */
.article-body code,
.article-body a[href^="http"] { overflow-wrap: anywhere; }

/* MEASURE. `ch` is the advance width of "0", which in Inter is ~1.28x the average
   glyph, so 72ch did NOT land in the 65-75 band — measured with canvas metrics
   against the widest rendered line it ran 727px / 91-96 characters per line on
   the site's primary reading surface. 60ch measures ~606px / ~77 cpl. Re-measure
   with real glyph metrics if this is ever changed; do not reason from `ch`. */
.article-body { max-width: 38em; }
/* Running prose sits one step inside the column so it lands at ~76 cpl, while
   figures and exhibits keep the fuller 38em measure they were composed at. */
.article-body > p,
.article-body > ul,
.article-body > ol,
.article-body > blockquote { max-width: 36em; }

/* h4 is the fourth rung of the hierarchy. Several source articles carry bare
   paragraphs acting as sub-sub-headings ("Reports + Dashboards", "Tier 1:
   Inbound Content Fuels Outbound Targeting"); promoting them needs a rhythm,
   and v2.css only sets one for h2 and h3. Slightly tighter than h3. */
.article-body h4 {
  margin-top: var(--space-6);
  margin-bottom: var(--space-2);
  font-size: 1.03rem;
  line-height: 1.35;
}

/* ==========================================================================
   1. THE FIGURE SHELL — .figx
   Every archetype below is dropped inside this shell. Nothing else.
     figure.figx
       div.figx__head  > span.figx__eyebrow ("Figure 3") + span.figx__title
                       + p.figx__sub (optional standfirst, <= 68ch)
       div.figx__plot  [role=img][aria-label="…text alternative…"]
       figcaption.figx__cap  > span.figx__src (optional source note)
   ========================================================================== */

/* the shell itself is styled in v2.css (G20). Additions only, below. */

.figx--lead {
  border-color: #cbd5e1;
  box-shadow: 0 10px 30px -18px rgba(17, 24, 39, 0.28), 0 1px 2px rgba(17, 24, 39, 0.04);
}
/* the dominant figure of a piece earns a little more air and a heavier rule */
.figx--lead .figx__head { border-bottom-width: 2px; border-bottom-color: var(--color-primary-bright); }

.figx__sub {
  margin: 0;
  max-width: 36em;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--color-text-muted);
}
/* .figx__plot deliberately sets NO `display`. It is a hook, not a layout. The
   archetypes defined in v2.css (figgrid, figrail, figrec) load BEFORE this file,
   so a `display: block` here would out-cascade their `display: grid` at equal
   specificity and silently flatten them to one column. */

/* styles.css sets `svg { fill: currentColor }` at (0,0,0,1). A presentation
   attribute loses to ANY author rule, so a plain fill="none" on an outline
   glyph silently renders as a solid blob. Class-level guard for the whole
   figure system, so no archetype has to remember. */
.figx svg[fill="none"] { fill: none; }

/* --------------------------------------------------------------------------
   FIGURE SCOPE — read this before adding an archetype.
   v2.css styles `.article-body p`, `ul`, `ol` and `li` at specificity (0,0,1,1).
   A single-class component selector is (0,0,1,0) and therefore LOSES, so inside
   a figure the prose rules quietly add a 16px paragraph margin, 1.2rem of list
   padding and a 6px margin on every list row. Those are exactly the values that
   break a diagram's alignment, and they do it silently.
   This block neutralises the box model and restores the intent of the
   components whose text sits directly in a bare <p> or <li>.
   ANY NEW ARCHETYPE with text in a bare <p> or <li> must be listed here.
   -------------------------------------------------------------------------- */
/* MARGIN only on the items: v2.css sets no padding on p or li, and resetting
   padding here would out-specify every archetype's own cell padding. */
.article-body .figx p,
.article-body .figx li,
.article-body .figx dl,
.article-body .figx dd { margin: 0; }
/* the list CONTAINERS do carry inherited padding, so those are reset */
.article-body .figx ul,
.article-body .figx ol { margin: 0; padding: 0; list-style: none; }

.article-body .figx .figx__sub { line-height: 1.55; }
.article-body .figx .figbars__lab { color: var(--color-text); line-height: 1.4; }
.article-body .figx .figbars__val { color: var(--color-primary); line-height: 1.4; }
.article-body .figx .figbars__axis { line-height: 1.4; }
.article-body .figx .figsplit__claim { color: var(--color-text); line-height: 1.35; }
.article-body .figx .figsplit__foot { line-height: 1.5; }
.article-body .figx .fighub__unit { color: var(--color-text); line-height: 1.35; }
.article-body .figx .figrec__v { color: var(--color-text); line-height: 1.55; }

/* the two lists inside a figure that really are lists */
.article-body .figx .figstack__list,
.article-body .figx .figflow__list { padding-left: 1.05rem; list-style: disc; }
.article-body .figx .figstack__list li,
.article-body .figx .figflow__list li { margin-block: 3px; }

/* --------------------------------------------------------------------------
   THE SAME SCOPE BUG, OUTSIDE .figx.
   The block above protects the figure system from `.article-body p / li / ul`
   (0,0,1,1) out-specifying single-class component selectors (0,0,1,0). The
   figures were guarded; the SHARED PARTIALS dropped into an article were not,
   so a component rendered one way on 44 pages and another way on the blog
   pages -- every instance individually legible, no two agreeing.

   MEASURED, same partial, blog page vs everywhere else:
     .tq__quote     #626977 not #111827, line-height 1.7 not 1.55, +16px below
                    (on /resources/blogs/revops-diagnostic-hubspot/ two Felix /
                    Martin testimonials render muted while the same partials
                    render ink on 16 other pages)
     .compare-list  +19.2px padding-left -- the list visibly indents
     .ci            line-height 26.4px not 24px
     .hint          line-height 21.2px not 20px, +16px below

   Restore each component's OWN intent rather than blanket-resetting, and match
   the article scope's specificity so the component wins.
   ANY SHARED PARTIAL placed inside .article-body must be listed here. */
.article-body .tq p.tq__quote {
  color: var(--color-text);
  line-height: 1.55;
  margin-bottom: 0;
}
/* .compare is NOT listed: its block margins on a blog page come from an inline
   `style="margin:var(--space-8) 0"` in the markup, which is placement intent,
   not a prose rule leaking in. A rule here could not win against it anyway. */
.article-body ul.compare-list { margin: 0; padding-left: 0; }
.article-body .ci { line-height: 24px; }
.article-body p.hint { line-height: 1.6; margin-bottom: 0; }

/* --- THREE MORE ESCAPES FROM THE SAME LEAK ----------------------------
   The list above caught the quote, the list container and the tick badge.
   It did not catch the pieces measured below, so the SAME components still
   rendered to a different rhythm on the one page that embeds them in prose
   than on the nine to sixteen pages that do not. Each one passes every
   per-element check on its own; the defect only exists as a disagreement
   with its own counterpart.

   MEASURED at 1280, /resources/blogs/revops-diagnostic-hubspot/ vs the
   pages that carry the same component outside an article:
     .compare-col h3    margin 32px above / 8px below, against 0 / 24px on
                        10 other instances. `.article-body h3` (0,0,1,1)
                        out-specifies `.compare-col h3` (0,0,1,1) on source
                        order alone, so the card heading gained 32px of lead
                        and lost 16px of the gap to its own list.
     .compare-list li   margin-block 6px and line-height 26.4px, against 4px
                        and 24px on 9 other instances. The rows sit 2px
                        further apart and each row is 2.4px taller, so a
                        six-row column runs ~26px longer than the identical
                        column on /about/.
     .callout p         margin-bottom 16px and colour #626977, against 0 and
                        #111827 on 8 other instances. `.callout p{color:
                        var(--color-text)}` is the component saying this is
                        emphasis, not body prose; `.article-body p` mutes it.

   Values restored from each component's own declaration, not re-picked:
   `.compare-col h3{margin-bottom:var(--space-6)}`, the base `li{margin-block:
   var(--space-1)}` with `.compare-list li{line-height:1.5}`, and
   `.callout p{color:var(--color-text)}`. */
.article-body .compare-col h3 { margin-top: 0; margin-bottom: var(--space-6); }
.article-body .compare-list li { margin-block: var(--space-1); line-height: 1.5; }
/* `inherit`, not a re-typed 1.65: outside an article the callout's prose takes
   the document line-height off its own box, and that is the value to match. */
.article-body .callout p { color: var(--color-text); margin-bottom: 0; line-height: inherit; }

.figx__cap {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--fig-rule);
  font-size: 0.8rem;
  font-style: italic;
  line-height: 1.55;
  color: var(--color-text-muted);
}
.figx__src {
  display: block;
  margin-top: 4px;
  font-style: normal;
  /* 12px floor. Figure source notes are reading text and shipped at 11.84px. */
  font-size: 0.75rem;
  letter-spacing: 0.01em;
  color: var(--color-text-muted);
}

/* full-bleed on small screens: the gutter is exactly var(--gutter), so these
   negative margins land on the viewport edge and cannot overflow. */
@media (max-width: 700px) {
  .figx--bleed {
    margin-inline: calc(var(--gutter) * -1);
    border-inline: 0;
    border-radius: 0;
  }
}

/* shared micro-typography used across the archetypes */
.fig-lab {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.94rem;
  line-height: 1.4;
  color: var(--color-text);
}
.fig-note {
  font-size: 0.84rem;
  line-height: 1.55;
  color: var(--color-text-muted);
}
.fig-micro {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.fig-num {
  font-family: var(--font-heading);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}

/* ==========================================================================
   THE VALUE-SHADE UTILITIES
   The ramp itself is declared once in styles.css. These six classes are the
   ONLY way any chart on the site selects a step, core pages and the EU
   vertical alike, so "which orange is this bar" is never a per-chart decision.

   .sh-1 is the highest value in its own chart; .sh-5 is the lowest. Equal
   values carry the SAME class, which is the whole point: before this existed,
   a category origin strip drew a 2 and a 1 in the identical bright orange and
   the fill encoded nothing at all.

   .sh-r is the pooled remainder. It sits outside the ordered ramp and lighter
   than .sh-5 so a bucket of leftovers can never read as outranking a value
   that was actually counted.

   A chart whose bars are a SEQUENCE rather than a magnitude (a timeline, a
   process order) must not take these classes: shading a timeline by value
   asserts that the last date is the biggest number, which is a false encoding.
   Such a figure keeps the single brand orange and says so with .figbars--seq.
   ========================================================================== */
.sh-1 { --shade: var(--chart-shade-1); }
.sh-2 { --shade: var(--chart-shade-2); }
.sh-3 { --shade: var(--chart-shade-3); }
.sh-4 { --shade: var(--chart-shade-4); }
.sh-5 { --shade: var(--chart-shade-5); }
.sh-r { --shade: var(--chart-shade-rest); }

/* The legend swatch. Its fill is the same step as the mark it explains, and
   its 1px keyline is what keeps the swatch itself perceivable (5.52:1 on
   white) once the ramp reaches its pale end. */
.sh-sw {
  display: inline-block;
  width: 14px;
  height: 10px;
  flex: none;
  border-radius: var(--radius-sm);
  background: var(--shade, var(--chart-shade-3));
  box-shadow: inset 0 0 0 1px var(--chart-keyline);
}

/* ==========================================================================
   ARCHETYPE 1 — .figbars : MAGNITUDE
   Serves: a set of figures the article itself quotes.
   Shape test: bar length alone ranks the claims.
   Bars are SLIM and the track is light grey. A range renders as a solid bar
   to its lower bound plus a tinted extension to its upper bound, so no single
   value is ever invented to stand in for a range.
   ========================================================================== */
.figbars { margin: 0; display: grid; gap: 2px; }
.figbars__row {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr) 5.5rem;
  align-items: center;
  gap: var(--space-4);
  padding: 13px 0;
  border-bottom: 1px solid var(--fig-rule);
}
.figbars__row:last-child { border-bottom: 0; }
.figbars__lab {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.92rem;
  line-height: 1.4;
  color: var(--color-text);
}
.figbars__track {
  position: relative;
  display: block;
  height: var(--fig-bar-h);
  border-radius: var(--radius-pill);
  background: var(--fig-rule);
  overflow: hidden;
}
/* --to = the quoted figure (or a range's lower bound), as a % of the axis.
   --shade comes from the row's .sh-N class and tracks the VALUE; a row with no
   .sh-N (a sequence figure, or any row authored before the ramp) falls back to
   step 3, which IS the brand orange, so nothing regresses. */
.figbars__fill {
  position: absolute;
  inset-block: 0;
  left: 0;
  width: var(--to, 0%);
  border-radius: var(--radius-pill);
  background: var(--shade, var(--chart-shade-3));
}
/* A timeline or an ordered process is not a magnitude ranking, so it keeps the
   single brand orange rather than asserting that its last step is its largest. */
.figbars--seq .figbars__fill,
.figbars--seq .figbars__ext { background: var(--chart-shade-3); }
/* --from/--to on the extension mark the top of a quoted range. It takes the
   row's own shade so the range and its lower bound stay one colour. It stays
   translucent so the extension still reads as the softer half of the same bar,
   but 0.3 over the pale end of the ramp came out invisible, so 0.42. */
.figbars__ext {
  position: absolute;
  inset-block: 0;
  left: var(--from, 0%);
  width: calc(var(--to, 0%) - var(--from, 0%));
  border-radius: var(--radius-pill);
  background: var(--shade, var(--chart-shade-3));
  opacity: 0.42;
}
.figbars__val {
  margin: 0;
  text-align: right;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-primary);
}
/* the scale sits under the TRACK column, never under the whole figure, or the
   tick marks line up with nothing */
.figbars__axis {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr) 5.5rem;
  gap: var(--space-4);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--fig-rule);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.figbars__ticks {
  grid-column: 2;
  display: flex;
  justify-content: space-between;
}
@media (max-width: 640px) {
  .figbars__row {
    grid-template-columns: minmax(0, 1fr) 5rem;
    row-gap: 9px;
    padding: 15px 0;
  }
  .figbars__lab { grid-column: 1 / -1; }
  .figbars__val { order: 2; }
  .figbars__axis { grid-template-columns: minmax(0, 1fr) 5rem; }
  .figbars__ticks { grid-column: 1; }
  .figbars__pad { display: none; }
}

/* ==========================================================================
   ARCHETYPE 2 — .figsplit : BEFORE / AFTER
   Serves: any two-state contrast the article draws (siloed vs aligned,
   old vs new). Two panels, each one self-contained inline SVG at a ~300 unit
   viewBox, so the rendered scale never falls below 1.0 and the labels inside
   stay legible at 390px.
   Shape test: scattered islands on the left, one continuous rail on the right.
   ========================================================================== */
.figsplit {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-5);
}
@media (max-width: 720px) { .figsplit { grid-template-columns: minmax(0, 1fr); } }
.figsplit__panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  border: 1px solid var(--fig-rule);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}
.figsplit__panel--after { background: var(--fig-cream); border-color: var(--fig-cream-deep); }
.figsplit__state {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;   /* 12px floor (was 11.20px) */
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.figsplit__panel--after .figsplit__state { color: var(--color-primary); }
.figsplit__claim {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.35;
  color: var(--color-text);
}
.figsplit__svg { width: 100%; height: auto; display: block; }

/* ---- Panel label size: DECLARED IS NOT DELIVERED -------------------------
   effective_px = declared_px x (rendered width / viewBox width).

   Measured rendered width of one panel SVG, at 28 viewport widths:

     viewport   360  390  430  480  520  560  600  640  700  719 | 721  768 1440
     1 column   240  270  308  354  390  427  464  504  520  520 |
     bleed      274  304  344  394  434  474  514  554  570  520 |
     2 columns                                                   | 229  228  217

   Two failures fell out of that table, at opposite ends of the same cause.
   Above 720px the panels sit side by side and render at 0.72x, so a declared
   16px delivered 11.57px, under the floor. Below 720px they go full width and
   the SAME 16px delivered 30.4px at 700px, larger than the panel's own claim
   heading and wide enough to burst the shapes it labels.

   The fix is in two parts. The SVG is capped at 1.333x its authored width once
   it is the only thing in the column, which stops the runaway; and the
   declared size is then tiered against the measured scale so the delivered
   size stays inside 12.2-16.0px at every width.   */
@media (max-width: 720px) {
  .figsplit__svg { max-width: 400px; margin-inline: auto; }
  .figsplit__svg[viewBox="0 0 270 112"] { max-width: 360px; }
}

/* styles.css sets `svg { fill: currentColor }`, so every mark below carries an
   explicit fill/stroke. Only the type is inherited. */
.figsplit__svg { --fslab: 15.25px; }              /* <= 390px: scale 0.80-1.01 */
@media (min-width: 391px) and (max-width: 430px) { .figsplit__svg { --fslab: 13.75px; } }
@media (min-width: 431px) and (max-width: 720px) { .figsplit__svg { --fslab: 12px; } }
@media (min-width: 721px) {
  .figsplit__svg { --fslab: 17px; }               /* two columns: scale 0.72-0.76 */
  /* The 270-unit figure renders at the same width as the 300-unit ones, so it
     scales 11% harder and needs 11% less declared size to land in the band. */
  .figsplit__svg[viewBox="0 0 270 112"] { --fslab: 15.25px; }
}
.figsplit__svg text {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: var(--fslab);
  fill: var(--color-text);
}
.figsplit__svg .fs-cap {
  font-weight: 700;
  /* Same size as the surrounding label, not a separate number: the caps read as
     caps through weight, colour and tracking. A second size here is what let
     the caps drift to 17px everywhere and clip against the viewBox edge. */
  font-size: var(--fslab);
  /* 0.06em, not 0.1em. Tracking is multiplied by the character count, so on a
     25-character caption 0.1em was adding 15 user units of width to a 300-unit
     drawing, which is what pushed three captions past their own edge. */
  letter-spacing: 0.06em;
  fill: var(--color-text-muted);
}
.figsplit__foot { margin: 0; font-size: 0.82rem; line-height: 1.5; color: var(--color-text-muted); }

/* ==========================================================================
   ARCHETYPE 3 — .fighub : ONE THING UNIFIES SEVERAL
   Serves: "X does not sit above or below A, B and C; it aligns them."
   Three units on a row, joined by drop rules to a single spanning base.
   Shape test: three separate boxes resting on one unbroken bar.
   ========================================================================== */
.fighub { display: grid; gap: 0; }
.fighub__row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.fighub__unit {
  margin: 0;
  padding: var(--space-4) var(--space-3);
  text-align: center;
  border: 1px solid var(--fig-rule);
  border-radius: var(--radius-sm);
  background: var(--color-surface-alt);
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.9rem;
  line-height: 1.35;
  color: var(--color-text);
}
/* The connector: a drop from each unit onto a bus, then one drop into the base.
   Drawn in CSS so the hairlines stay crisp at any width and any pixel ratio.
   The bus must end on the CENTRE of the outer units, and with a 3-column grid
   those centres are not at 1/6 and 5/6 once the gap is taken out, so the offset
   is computed from the gap rather than guessed. */
.fighub__join {
  --fighub-inset: calc((100% - 2 * var(--space-3)) / 6);
  position: relative;
  height: 34px;
}
.fighub__join::before {
  content: "";
  position: absolute;
  top: 0;
  height: 17px;
  left: var(--fighub-inset);
  right: var(--fighub-inset);
  background:
    linear-gradient(var(--color-teal), var(--color-teal)) left top / 1px 100% no-repeat,
    linear-gradient(var(--color-teal), var(--color-teal)) center top / 1px 100% no-repeat,
    linear-gradient(var(--color-teal), var(--color-teal)) right top / 1px 100% no-repeat,
    linear-gradient(var(--color-teal), var(--color-teal)) left bottom / 100% 1px no-repeat;
}
.fighub__join::after {
  content: "";
  position: absolute;
  top: 17px;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: var(--color-teal);
}
.fighub__base {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: center;
  text-align: center;
  padding: var(--space-5);
  border: 1px solid var(--color-teal);
  border-radius: var(--radius-sm);
  background: var(--color-teal-soft);
}
.fighub__name {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.02rem;
  color: var(--color-teal);
}
.fighub__role { font-size: 0.85rem; line-height: 1.5; color: var(--color-text-muted); max-width: 46ch; }
/* a unit that carries the article's own gloss under its label. Long glosses do
   not read centred, so --rich left-aligns the row. */
.fighub__unit .fig-note { display: block; margin-top: 6px; font-weight: 400; }
.fighub--rich .fighub__unit { text-align: left; }
@media (max-width: 560px) {
  .fighub__row { grid-template-columns: minmax(0, 1fr); gap: 6px; }
  .fighub__unit { text-align: left; }
  /* one column, so the connector is a single unbroken drop */
  .fighub__join { height: 24px; }
  .fighub__join::before {
    left: 50%;
    right: 50%;
    height: 12px;
    background: linear-gradient(var(--color-teal), var(--color-teal)) center top / 1px 100% no-repeat;
  }
  .fighub__join::after { top: 12px; }
}

/* ==========================================================================
   ARCHETYPE 4 — .figflow : STAGED PROCESS
   Serves: an ordered sequence the article walks through.
   Chevrons carry direction, so the order is readable with the labels removed.
   Horizontal for 3-4 stages; add --stack for 5+ (vertical chevrons), which is
   also what every variant collapses to at <= 760px.
   ========================================================================== */
.figflow {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 3px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.figflow__step {
  margin: 0;
  padding: var(--space-4) var(--space-4) var(--space-4) calc(var(--fig-notch) + var(--space-4));
  background: var(--color-surface-alt);
  clip-path: polygon(
    0 0, calc(100% - var(--fig-notch)) 0, 100% 50%,
    calc(100% - var(--fig-notch)) 100%, 0 100%, var(--fig-notch) 50%
  );
}
.figflow__step:first-child {
  padding-left: var(--space-4);
  clip-path: polygon(0 0, calc(100% - var(--fig-notch)) 0, 100% 50%, calc(100% - var(--fig-notch)) 100%, 0 100%);
}
.figflow__step:last-child {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, var(--fig-notch) 50%);
}
.figflow__step:only-child { clip-path: none; }
.figflow__step--key { background: var(--fig-cream-deep); }
.figflow__n {
  display: block;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;   /* 12px floor (was 10.56px) */
  letter-spacing: 0.1em;
  color: var(--color-primary);
  font-variant-numeric: tabular-nums;
  margin-bottom: 5px;
}
.figflow__lab {
  display: block;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.92rem;
  line-height: 1.35;
  color: var(--color-text);
}
.figflow__txt { display: block; margin-top: 6px; font-size: 0.84rem; line-height: 1.55; color: var(--color-text-muted); }
/* a stage whose content really is a list of tasks, not a sentence. Only ever
   used inside --stack, because a bulleted list in a 160px horizontal chevron
   is unreadable. Mirrors .figstack__list so the two read identically. */
.figflow__list { margin: 6px 0 0; padding-left: 1.05rem; }
.figflow__list li { font-size: 0.84rem; line-height: 1.55; color: var(--color-text-muted); margin-block: 3px; }

/* vertical chevrons — the default for 5+ stages and for every narrow screen.
   A deeper notch, because a 13px V read across a 640px block is nearly flat. */
.figflow--stack,
.figflow--stack.figflow { grid-auto-flow: row; grid-auto-columns: auto; --fig-notch: 22px; }
.figflow--stack .figflow__step,
.figflow--stack .figflow__step:first-child,
.figflow--stack .figflow__step:last-child {
  /* the block padding has to clear the notch or the chevron eats the text */
  padding: calc(var(--fig-notch) + 10px) var(--space-5);
  clip-path: polygon(
    0 0, 50% var(--fig-notch), 100% 0,
    100% calc(100% - var(--fig-notch)), 50% 100%, 0 calc(100% - var(--fig-notch))
  );
}
.figflow--stack .figflow__step:first-child {
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--fig-notch)), 50% 100%, 0 calc(100% - var(--fig-notch)));
}
.figflow--stack .figflow__step:last-child {
  clip-path: polygon(0 0, 50% var(--fig-notch), 100% 0, 100% 100%, 0 100%);
}
@media (max-width: 760px) {
  .figflow { grid-auto-flow: row; grid-auto-columns: auto; --fig-notch: 16px; }
  .figflow .figflow__step,
  .figflow .figflow__step:first-child,
  .figflow .figflow__step:last-child {
    padding: calc(var(--fig-notch) + 10px) var(--space-5);
    clip-path: polygon(
      0 0, 50% var(--fig-notch), 100% 0,
      100% calc(100% - var(--fig-notch)), 50% 100%, 0 calc(100% - var(--fig-notch))
    );
  }
  .figflow .figflow__step:first-child {
    clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--fig-notch)), 50% 100%, 0 calc(100% - var(--fig-notch)));
  }
  .figflow .figflow__step:last-child {
    clip-path: polygon(0 0, 50% var(--fig-notch), 100% 0, 100% 100%, 0 100%);
  }
}

/* ==========================================================================
   ARCHETYPE 5 — .figstack : LAYERS
   Serves: a foundation-and-layers argument, or a ladder that widens with
   scale. --ladder sets each row's width from --w, so size is legible before
   any label is read. Widths stay >= 78% on mobile so labels never crush.
   ========================================================================== */
.figstack { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
.figstack__layer {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 4px;
  margin: 0;
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--fig-rule);
  border-left: 3px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}
.figstack__layer--base {
  background: var(--fig-cream);
  border-color: var(--fig-cream-deep);
  border-left-color: var(--color-primary-bright);
}
.figstack--ladder .figstack__layer { width: var(--w, 100%); }
@media (max-width: 620px) {
  .figstack--ladder .figstack__layer { width: max(78%, var(--w, 100%)); }
}
.figstack__lab {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.94rem;
  line-height: 1.35;
  color: var(--color-text);
}
.figstack__txt { font-size: 0.85rem; line-height: 1.55; color: var(--color-text-muted); }
.figstack__list { margin: 2px 0 0; padding-left: 1.05rem; }
.figstack__list li { font-size: 0.85rem; line-height: 1.55; color: var(--color-text-muted); margin-block: 3px; }

/* ==========================================================================
   ARCHETYPE 6 — .figcheck : CHECKLIST AS DIAGRAM
   Serves: genuine criteria, requirements or tests. The ring-and-tick mark is
   only ever used where the article really is listing things to check for.
   ========================================================================== */
.figcheck {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1px;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--fig-rule);
  border: 1px solid var(--fig-rule);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.figcheck__item {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  align-items: start;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-4) var(--space-5);
  background: var(--color-surface);
}
.figcheck__mark { width: 22px; height: 22px; display: block; color: var(--color-teal); fill: none; }
.figcheck__lab {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--color-text);
}

/* --------------------------------------------------------------------------
   ODD COUNTS IN THE TWO-COLUMN SETS (.figgrid, .figcheck)
   Both paint their 1px seams by showing the container background through the
   gaps. With an odd number of cells the final slot is EMPTY, so the container
   background paints a bare grey block next to the last cell. Span the leftover
   cell across the row instead. Even counts are untouched, and at one column
   `grid-column: 1 / -1` is a no-op.
   -------------------------------------------------------------------------- */
.figgrid__cell:last-child:nth-child(odd),
.figcheck__item:last-child:nth-child(odd) { grid-column: 1 / -1; }

/* ==========================================================================
   ARCHETYPE 7 — .figfunnel : NARROWING STAGES
   Serves: a genuine funnel (top/middle/bottom, or a stage-by-stage narrowing
   the article describes). The band silhouette is drawn per row so the labels
   sit outside the shape and stay full size on any screen.
   Tints are fill-opacity on #F97316. No new hue is introduced.
   ========================================================================== */
/* 2px, not a space token: the bands have to stack into ONE silhouette. At a
   12px gap the reader sees four separate trapezoids and the funnel stops
   being a funnel, which is the whole argument the archetype carries. */
.figfunnel { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
.figfunnel__stage {
  display: grid;
  grid-template-columns: 104px minmax(0, 1fr);
  align-items: center;
  gap: var(--space-5);
  margin: 0;
  min-height: 62px;
}
.figfunnel__band { display: block; width: 100%; height: 100%; min-height: 62px; }
.figfunnel__lab {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.95rem;
  line-height: 1.35;
  color: var(--color-text);
}
.figfunnel__txt { display: block; margin-top: 3px; font-size: 0.84rem; line-height: 1.55; color: var(--color-text-muted); }
@media (max-width: 560px) {
  .figfunnel__stage { grid-template-columns: 58px minmax(0, 1fr); gap: var(--space-4); }
}

/* ==========================================================================
   ARCHETYPE 8 — .figmatrix : TWO AXES
   Serves: a real 2x2 the article draws. Stays 2x2 at every width, because a
   stacked matrix is not a matrix. Keep quadrant labels under ~40 characters.
   ========================================================================== */
.figmatrix {
  display: grid;
  grid-template-columns: 26px repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr)) 26px;
  gap: 4px;
}
.figmatrix__y {
  grid-row: 1 / 3;
  display: grid;
  place-items: center;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.figmatrix__x {
  grid-column: 2 / 4;
  display: grid;
  place-items: center;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.figmatrix__cell {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: var(--space-4);
  border: 1px solid var(--fig-rule);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  min-height: 92px;
}
.figmatrix__cell--key { background: var(--fig-cream); border-color: var(--fig-cream-deep); }
.figmatrix__pos {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.figmatrix__cell--key .figmatrix__pos { color: var(--color-primary); }
.figmatrix__lab {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.9rem;
  line-height: 1.3;
  color: var(--color-text);
}
.figmatrix__txt { font-size: 0.8rem; line-height: 1.5; color: var(--color-text-muted); }
@media (max-width: 560px) {
  .figmatrix { grid-template-columns: 20px repeat(2, minmax(0, 1fr)); gap: 3px; }
  .figmatrix__cell { padding: var(--space-3); min-height: 78px; }
  .figmatrix__lab { font-size: 0.82rem; }
  .figmatrix__txt { font-size: 0.75rem; }
}

/* ==========================================================================
   ARCHETYPE 9 — .figtime : MILESTONES
   Serves: dated events the article states. Horizontal spine on wide screens,
   vertical spine below 720px. Never used for undated sequences — that is
   .figflow's job.
   ========================================================================== */
.figtime {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.figtime__point {
  position: relative;
  margin: 0;
  padding-top: 26px;
}
.figtime__point::before {
  content: "";
  position: absolute;
  top: 9px;
  left: 0;
  right: calc(var(--space-4) * -1);
  height: 1px;
  background: var(--fig-rule);
}
.figtime__point:last-child::before { right: 0; }
.figtime__point::after {
  content: "";
  position: absolute;
  top: 5px;
  left: 0;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--color-primary-bright);
}
.figtime__when {
  display: block;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--color-primary);
  font-variant-numeric: tabular-nums;
  margin-bottom: 4px;
}
.figtime__what { display: block; font-size: 0.87rem; line-height: 1.5; color: var(--color-text); }
@media (max-width: 720px) {
  .figtime { grid-auto-flow: row; grid-auto-columns: auto; gap: 0; }
  .figtime__point { padding: 0 0 var(--space-5) var(--space-6); }
  .figtime__point::before { top: 6px; bottom: 0; left: 4px; right: auto; width: 1px; height: auto; }
  .figtime__point:last-child::before { display: none; }
  .figtime__point::after { top: 4px; left: 0; }
}

/* ==========================================================================
   ARCHETYPE 10 — .dtable--compare : COMPARISON TABLE AS FIGURE
   Serves: a set of paired statements (symptom -> response, option A vs B).
   The wrapper .table-wrap already scrolls on its own, so a wide table never
   pushes the page sideways. First column reads as a row header.
   ========================================================================== */
.figx .table-wrap { margin: 0; border: 0; }
.dtable--compare { font-size: 0.89rem; }
.dtable--compare th { background: var(--color-surface); border-bottom: 2px solid var(--fig-rule); }
/* .dtable th sets `white-space: nowrap` in v2.css, which is right for a data
   header ("Q3") and wrong for a comparison header, which is a sentence. With
   nowrap the column head alone set the table's minimum width and the figure
   scrolled sideways inside its wrapper on a phone. */
.dtable--compare thead th { white-space: normal; }
.dtable--compare tbody th {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.89rem;
  text-transform: none;
  letter-spacing: 0;
  white-space: normal;
  color: var(--color-text);
  background: var(--fig-cream);
  border-bottom: 1px solid var(--fig-rule);
  width: 44%;
}
.dtable--compare tbody tr:last-child th,
.dtable--compare tbody tr:last-child td { border-bottom: 0; }
@media (max-width: 560px) {
  .dtable--compare tbody th { width: 46%; }
}

/* ==========================================================================
   HOUSEKEEPING
   No archetype animates. The site-wide reduced-motion block already disables
   the .reveal transition; this is belt and braces for the figure shell.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .figx,
  .figx * { transition: none !important; animation: none !important; }
}

/* ==========================================================================
   12. RESOURCE-INDEX MOTIFS — .se-motif primitives
   The listing cards on /resources/, /resources/blogs/ and /resources/events/
   each carry the SHAPE of the dominant diagram inside the piece they link to.
   That is the whole rule: a card's motif is drawn from an archetype the
   article actually contains, so the index previews the geometry of the
   argument rather than decorating the row.

   These are the drawing primitives. The per-card SVG lives in the page, not
   here, because each card names a different archetype.

   TRUTH: a motif is aria-hidden and carries no axis, no scale, no number and
   no label. It asserts nothing. The visible chip next to it names the
   archetype, which is a checkable fact about the linked page.

   GEOMETRY BUDGET: viewBox is 320x200 (= the 16/10 desktop tile exactly). At
   <=860px the tile becomes 16/7 and preserveAspectRatio="slice" crops to
   y 30..170, so every mark below must live inside y 36..164.

   The base .sm-bar / .sm-line / .sm-rule / .sm-node primitives are in v2.css.
   figures.css loads after v2.css, so same-specificity rules here win ties.
   Every stroked path carries a class that sets `fill: none` — the global
   `svg { fill: currentColor }` rule out-specifies a bare fill="none"
   presentation attribute and would otherwise flood the outline shapes.
   ========================================================================== */

/* bar track: always the light grey rule colour, always slim */
.se-motif .sm-track { fill: #e4eaf2; }
/* v2.css sets the hairline at #E2E8F0, which disappears on the slate tint */
.se-motif .sm-rule { stroke: #d5dee9; }
/* the filled part of a bar / a solid block */
.se-motif .sm-fill  { fill: #cbd5e1; }
/* the one accented mark per motif — a tint of the brand orange, never a new hue */
.se-motif .sm-fill--accent { fill: var(--color-primary-bright); fill-opacity: 0.34; }
.se-motif .sm-fill--teal   { fill: var(--color-teal); fill-opacity: 0.20; }
/* white plate under a record-style motif, so the key/value rows read as a card */
.se-motif .sm-plate { fill: #ffffff; fill-opacity: 0.72; stroke: #e2e8f0; stroke-width: 1; }
/* outline shape: chevrons, funnel walls, ladder rails */
.se-motif .sm-shape {
  fill: #e2e8f0;
  fill-opacity: 0.9;
  stroke: none;
}
.se-motif .sm-shape--accent { fill: var(--color-primary-bright); fill-opacity: 0.30; }
/* a stroked mark that must never take the inherited currentColor fill */
.se-motif .sm-stroke {
  fill: none;
  stroke: #cbd5e1;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.se-motif .sm-stroke--accent { stroke: var(--color-primary-bright); opacity: 0.6; }
.se-motif .sm-stroke--teal   { stroke: var(--color-teal); opacity: 0.5; stroke-width: 2.4; }

/* An event card carries BOTH a date and the motif. Centring the date drops it
   straight onto the middle of the geometry, where the record motif's plate and
   the funnel's waist both live. It goes to the top-left corner instead, laid
   out on one line, so the middle of the tile stays clear for the shape and the
   two labels take one corner each. */
.se-date--onmotif {
  position: absolute;
  z-index: 1;
  top: 12px;
  left: 14px;
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  padding: 7px 13px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid var(--color-border);
  text-align: left;
}
.se-date--onmotif .se-day { font-size: 1.05rem; }
.se-date--onmotif .se-mon { display: inline; margin-top: 0; }

/* The archetype chip. Same corner as .se-vis__tag but reads as a caption of
   the geometry rather than a repeat of the category above the headline. */
.se-vis__shape {
  position: absolute;
  z-index: 1;
  left: 14px;
  bottom: 12px;
  right: 14px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted-strong);
}
.se-vis__shape span {
  display: inline-block;
  max-width: 100%;
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  padding: 5px 12px;
  overflow-wrap: break-word;
}
@media (max-width: 380px) {
  .se-vis__shape { font-size: 0.75rem; }
}

/* ==========================================================================
   13. PAGE-LEVEL HORIZONTAL CLIP
   Found by the render-time sweep, not by audit.mjs: /work/, /expertise/ and
   /expertise/enablement-processes/ scrolled 5px sideways at exactly 1280px.
   The source is a paint-time overflow inside .app-frame.gfx-anim (a transform
   on the animated illustration), which nothing on the page clips, so it
   propagates all the way to <html>.

   `overflow-x: clip` and NOT `hidden`: clip does not create a scroll container,
   so the sticky .article-nav rail on every article page keeps working. `hidden`
   would silently kill it, which is the trap this comment exists to prevent.
   ========================================================================== */
main { overflow-x: clip; }

/* --- figevening: a left-to-right sequence of stages across one session ------
   Used by the meetup page. Follows the .figsplit__svg contract: the SVG scales
   to the plot width and keeps its own aspect, so nothing can collapse to 0
   height or push the page into horizontal scroll. Below 720px the viewBox is
   too wide to stay legible, so the figure scrolls INSIDE its own box rather
   than scrolling the page. -------------------------------------------------- */
.figevening { width: 100%; }
.figevening__svg { width: 100%; height: auto; display: block; }
/* Same rule as .figsplit: the label size written here is multiplied by
   (rendered width / 660) before the reader sees it. Measured: the SVG is pinned
   at 660px up to 719, then runs 627px (721) to 1082px (>=1200), i.e. 0.95x to
   1.64x. An 11px eyebrow therefore delivered 10.4-11.2px below 820px, under the
   floor, while a 15px title delivered 24.6px on a desktop. One variable now
   drives all three roles so the hierarchy cannot drift apart again. */
.figevening__svg { --evlab: 13px; }
@media (min-width: 960px) { .figevening__svg { --evlab: 9.5px; } }
.figevening__svg .ev-cap,
.figevening__svg .ev-body  { font-size: var(--evlab); }
.figevening__svg .ev-title { font-size: calc(var(--evlab) * 1.25); }
@media (max-width: 720px) {
  .figevening { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .figevening__svg { width: 660px; max-width: none; }
}


/* ==========================================================================
   FINAL SWEEP — LINE MEASURE
   Loaded last on purpose: these are caps on prose that component rules set to
   the full container. Measured with canvas glyph metrics against the widest
   rendered line, not by counting `ch`.

   Before: .callout p, .check-list li, .msys-item p and .hint inherited the full
   1026-1152px container and ran 114-166 characters per line, roughly double the
   60-80 target, on 14 pages. .check-list was the worst case because it rendered
   at TWO different measures on one page (544px and 1152px on
   /expertise/ai-personalisation/), so identical bullets read at 68 cpl in one
   section and 145 cpl in another.

   The cap is expressed in px because that is what was measured. 640px at 16px
   Inter is ~80 cpl, the top of the band; 16px prose in a narrow column is
   unaffected because max-width only ever removes width, never adds it.
   ========================================================================== */
/* The cap is in `em`, not px, because characters-per-line is a function of the
   font size: a 640px box holds ~84 characters at 16px but ~110 at 12.5px, so a
   single px cap left .hint running 108-112 cpl while 16px prose ran 84. At the
   site's body face one character averages ~0.475em, so 36em is ~76 cpl at EVERY
   size the cap applies to. Measured with canvas glyph metrics, not counted in `ch`. */
.callout p,
.callout li,
.check-list,
.msys-item p,
.msys-for,
.hint,
.uw-note,
p.muted:not(.footer-bottom p):not(.se-meta),
.tq__quote,
.webframe .wf-p { max-width: 36em; }
/* A centred block keeps its optical centre when the measure bites. */
.text-center .callout p,
.text-center .hint,
.section-head--mid .hint { margin-inline: auto; }
/* .check-list is a grid; capping the list caps every row identically, which is
   what removes the two-measures-on-one-page split. */
.check-list { max-width: 36em; }

/* Legal prose is reading text too. /privacy/ and /terms/ set bare <p> and <li>
   to the full .container--narrow (820px) and ran 96-104 cpl — the longest lines
   on the site after the fix. Same 36em measure, same reason. */
.container--narrow > p,
.container--narrow > ul > li,
.container--narrow > ol > li,
.feature-list li,
.next-list li { max-width: 36em; }

/* Orphan control on lead paragraphs — six leads ended on a single short word
   ("fluff.", "in.", "now.", "offers", "teams", "out.", "rest."). text-wrap:pretty
   is a progressive enhancement: browsers without it render exactly as before. */
.lead, .hero-sub, .article-body p { text-wrap: pretty; }


/* `ch` is the width of "0" and overshoots the average glyph by ~1.28x, so every
   measure on the site that was expressed in `ch` ran long: 64ch = 81 cpl,
   68ch = 86, 72ch = 91. All prose measures are now in `em`, where 36em is ~76
   cpl at any size. Two paragraphs on /work/ still carry an INLINE
   style="max-width:70ch" / "66ch" in the page source and no stylesheet can
   reach them; they belong to the page owner, not to this layer. */

/* =====================================================================
   V3.2 — DESIGN-DIRECTOR SWEEP (measured in-browser, 1280 / 1201 / 390)
   Scope: /expertise/ + its five children, /about/, /diagnostic/,
   /request-audit/, /contact/. Every rule below is a fix for a defect
   that was MEASURED on the rendered page, not inferred from source.
   ===================================================================== */

/* ---------------------------------------------------------------------
   1. A NARROW PAGE HERO CANNOT HOLD A SIDE-BY-SIDE CTA PAIR AT ANY WIDTH.

   `.page-hero .container` is capped at 880px at EVERY viewport, so its
   text column is ~422px however wide the window is. The CTA pair is
   `width:max-content` (correct: equal means "both match the widest
   label"), and its 629.5px max-content becomes the MINIMUM of that
   1.1fr track. The track therefore refuses to shrink and the whole
   .split overflows its own container by 208px, carrying the hero
   mockup with it.

   The existing stack rule capped the band at 1200px, which fixed only
   part of the range. MEASURED on /expertise/revops-crm/:
     1201px  hero mockup clipped 19px, floating chip clipped 37px
     1240px  mockup clipped  2px, chip clipped 20px
     1280px  mockup 16px from the window edge, chip clipped  2px
   (all clipped silently by body{overflow-x:hidden} — "Synced" and
   "conflicting records" were cut mid-word.)

   A narrow page hero has no upper edge to that band, so neither does
   the rule. The wide hero (.page-hero--wide, --container 1200px) and
   the home hero keep the original 821-1200 band below.
   --------------------------------------------------------------------- */
@media (min-width: 821px) {
  .page-hero:not(.page-hero--wide) .split .cta-row:has(> .btn + .btn):not(:has(> :not(.btn))) {
    grid-auto-flow: row;
    grid-auto-rows: 1fr;
    width: max-content;
    max-width: 100%;
  }
}

/* ---------------------------------------------------------------------
   2. THE CTA PAIR'S WIDTH WAS BEING SET BY A NOTE, NOT BY ITS LABELS.

   `.promo__cta` is `width:max-content` so the buttons size to the
   widest LABEL. The panel note lives inside that same box, so on the
   two pages whose note is long the note won the max-content contest:
     /expertise/growth-channels/   519px buttons
     /services/growth-marketing/   519px buttons
     everywhere else               299-307px buttons
   Same component, same two actions, 1.7x apart. Zeroing the note's
   width contribution (width:0 + percentage min-width, which is
   indefinite under max-content) hands the sizing back to the labels;
   the note still fills the resolved box and wraps inside it.
   --------------------------------------------------------------------- */
.promo__cta > .promo__note { width: 0; min-width: 100%; }
/* On phones a stacked pair fills its column everywhere else on the site
   (.cta-row goes width:100% below 640). The promo stack kept max-content
   and rendered 263px buttons inside a 306px panel. */
@media (max-width: 640px) {
  .promo__cta { width: 100%; }
}

/* ---------------------------------------------------------------------
   3. A CHECK-LIST THAT IS A DIRECT CHILD OF .container IS A FULL-WIDTH
      SECTION BLOCK, NOT A COLUMN BESIDE SOMETHING.

   `.check-list{max-width:36em}` is the right measure cap inside a split.
   Standing alone in a 1200px container it rendered 576px wide against
   the container's 1200px and hugged the left edge, so the right HALF of
   the section was empty under a full-width diagram — the block read as
   a broken two-column layout. Six pages: /expertise/ and its five
   children. Laying it on auto-fit tracks makes the block span its own
   section and keeps every row inside a comfortable measure (two items
   resolve to 2 x ~548px, three to 3 x ~352px).
   --------------------------------------------------------------------- */
@media (min-width: 900px) {
  .container > .check-list {
    max-width: none;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    column-gap: var(--space-8);
    align-items: start;
  }
}

/* ---------------------------------------------------------------------
   4. THE TESTIMONIAL GROUP TOUCHED WHATEVER CAME BEFORE IT.

   MEASURED gap between .tq-group and its previous sibling: 0px on
   /expertise/, /expertise/revops-crm/, /expertise/strategy-gtm/,
   /expertise/ai-personalisation/ and /about/ (where two carousels sat
   flush against each other). On /expertise/strategy-gtm/ the quote card
   and the mockup above it shared an edge and read as one broken object.
   Every other sibling in those containers is separated by 48-64px.
   .section-head already carries its own 64px bottom margin, so it keeps
   its spacing and does not get a second one.
   --------------------------------------------------------------------- */
* + .tq-group { margin-top: var(--space-12); }
.section-head + .tq-group { margin-top: 0; }
/* .pullquote had the same 0px seam against the card grid above it on
   /expertise/growth-channels/ and /expertise/strategy-gtm/. */
.container > * + .pullquote { margin-top: var(--space-12); }
.section-head + .pullquote { margin-top: 0; }

/* ---------------------------------------------------------------------
   5. ONE CARD, TWO INTERNAL RHYTHMS.

   A card title was followed by 8px of space where the page author wrote
   <p class="muted mt-2"> and by 0px where they wrote a bare <p>. Both
   spellings appear on the same page (/expertise/revops-crm/: the five
   "What lands on your desk" tiles are glued, the three outcome tiles are
   not), so the component's spacing depended on how the markup happened
   to be typed. One declared value; mt-2 already resolves to the same
   8px, so nothing that was correct moves.
   --------------------------------------------------------------------- */
.card :is(h2, h3, h4) + p { margin-top: var(--space-2); }

/* ---------------------------------------------------------------------
   6. THE TERMINAL LINK IN A CARD SAT AT A DIFFERENT HEIGHT IN EVERY CARD
      OF THE SAME ROW.

   The cards in a row are already equal height, but their closing link
   floated up to wherever the body copy ended, so a three-card row showed
   its three "See the service" links on three different baselines
   (MEASURED spread 56px on /expertise/). Pushing the last link to the
   bottom of the card puts them on one line without touching any copy.
   --------------------------------------------------------------------- */
.card--tile { display: flex; flex-direction: column; }
/* padding, not margin, carries the MINIMUM separation: an auto margin
   resolves to 0 in the tallest card of the row, which would otherwise
   delete the mt-6 the markup asked for and glue the link to the copy. */
.card--tile > .link-arrow:last-child,
.card--tile > .cta-row:last-child,
.card--tile > .card-links:last-child {
  margin-top: auto;
  padding-top: var(--space-6);
}
/* THE ARROW ON A WRAPPED CARD LINK FLOATED AWAY FROM ITS TEXT.
   .link-arrow is an inline-flex box, so on a link short enough to fit
   ("How I sharpen GTM ->") the arrow follows the words, but on a link that
   wraps ("See where this shows up: Fractional Marketing Lead") the arrow
   follows the TEXT BOX and lands at the far right of the card, vertically
   centred across both lines, ~180px from the last word. Two arrow
   behaviours in the same card, decided by label length. Inside a card the
   arrow now flows with the text, so it always sits after the last word.
   Scoped to cards: elsewhere .link-arrow sits in a row where the flex box
   is doing real work. */
.card--tile .link-arrow { display: inline; }
.card--tile .link-arrow svg { display: inline-block; vertical-align: -3px; margin-left: 4px; }

/* ---------------------------------------------------------------------
   7. THE JOURNEY RAIL DANGLED PAST THE FIRST AND LAST NODE.

   The connecting rail was pinned at left:8%/right:8% for every step
   count, which is not where the end badges are. On a three-step journey
   (900px, badge centres at 150 / 450 / 750) the rail started at 72px
   and ended at 828px, so 44px of dashed line hung in the air off each
   end. A rail runs BETWEEN nodes: half a column in from each side, which
   is exactly 100%/(2n).
   --------------------------------------------------------------------- */
.journey::before      { left: 12.5%;   right: 12.5%; }     /* 4 columns */
.journey--3::before   { left: 16.667%; right: 16.667%; }   /* 3 columns */
.journey--5::before   { left: 10%;     right: 10%; }       /* 5 columns */
@media (max-width: 980px) {
  .journey--5::before { left: 16.667%; right: 16.667%; }   /* falls back to 3 */
}
@media (max-width: 860px) {
  .journey::before, .journey--3::before, .journey--5::before { left: 25%; right: 25%; }
}

/* ---------------------------------------------------------------------
   8. A DECORATIVE CHIP WAS PAINTING OVER FIGURE DATA.

   .gfx-chip--bl is positioned bottom:-18px and, at 57px tall, reaches
   39px UP into the frame it sits on — past the 16px body padding and
   into the last row of content. MEASURED on /diagnostic/: the chip
   "2-3 fixes worth making first" covered the label "Weeks 7-12" by
   72 x 9px, and it carries z-index 3, so the label lost. Reserving the
   chip's own intrusion at the foot of the body is the fix; nothing
   moves on a frame that has no bottom chip.
   --------------------------------------------------------------------- */
.app-frame:has(> .gfx-chip--bl) .aw-body { padding-bottom: 52px; }
@media (max-width: 640px) {
  .app-frame:has(> .gfx-chip--bl) .aw-body { padding-bottom: 44px; }
}

/* ---------------------------------------------------------------------
   9. TWO CLIENT LOGOS WERE EFFECTIVELY INVISIBLE ON THE CREAM BAND.

   The strip rendered every mark with grayscale(1) at opacity .74, which
   preserves the source art's own lightness. MEASURED effective contrast
   of each mark's darkest ink against the cream band (#FEF3C7, L 241):
     curioinvest 3.01   quantamix 3.14   breachlock 2.88   co-flo 2.12
     settly      1.32   bijles    1.43
   1.3:1 is not a logo, it is a smudge — and it is the same defect class
   as the white-on-cream marks that were fixed earlier, arriving from the
   other end of the scale. Every one of these six PNGs is transparent
   art with NO light counters (measured: 0% of opaque pixels above
   L=200), so a single-ink silhouette loses no shape and gives the
   uniform logo wall the brief asked for. Black at .72 over the cream
   band measures 3.16:1. Hover still restores the mark in full colour.
   The on-dark strip inherits the same treatment so there is exactly one
   client-logo appearance on the site.

   THE PARTNER STRIP NOW SHARES THIS RULE (Sushil, 02.08: "why are the
   certified and training partner animation not as good as the others ...
   it's too dark, make it as sleek and smooth as the Trusted by B2B Tech &
   SaaS teams logos"). It used to run grayscale(1) brightness(.55) at
   opacity 1, which PRESERVES each mark's own lightness instead of removing
   it: measured on the #F1F5F9 band, moengage landed at ink L=20 and
   crawlq at L=30 while activecampaign sat at L=125 and phantombuster at
   L=110. Half the strip read as too dark and half as washed out, against
   the client strip's single flat L=67. One rule, one ink, both strips.

   PRECONDITION, and it is the whole reason this could not simply be
   copied across earlier: brightness(0) maps every OPAQUE pixel to black,
   so a counter PAINTED WHITE fills in and the mark becomes a blob.
   Measured on the old assets, 82.9% of activecampaign's ink was above
   L=200, hubspot 51.2%, phantombuster 39.3%, customer.io 30.7% - all four
   collapsed into solid shapes under this rule. Every partner PNG has since
   been rebuilt with its counters KNOCKED OUT to alpha 0 rather than filled,
   which is the ordinary one-colour derivation of a mark and is what makes
   one rule safe for both rosters. Do not add a mark to either strip
   without checking it the same way: composite it on the band, apply the
   rule, and look at it.

   THE PLATE TILES ON /resources/ NOW SHARE IT TOO. They were left
   on the superseded grayscale(1) brightness(.55) at opacity 1 when
   the two loops moved here, and cogoport.png and settly.png render
   in BOTH places on that page - the same two marks, two different
   inks, one scroll apart. A treatment that is "the same as the two
   loops" has to BE the two loops' rule, not a copy of what it said
   at the time. This selector list is the only place on the site
   that sets a logo mark's filter or opacity; the transition rides
   with it for the same reason.
   --------------------------------------------------------------------- */
.trust-mq__item img,
.trust-logos img,
.trust-strip--on-dark .trust-mq__item img,
.partner-mq__item img,
.se-vis--plate img,
.case-mark img {
  filter: grayscale(1) brightness(0);
  opacity: 0.72;
  transition: filter 0.25s var(--ease-out), opacity 0.25s var(--ease-out);
}
.trust-mq__item img:hover,
.trust-logos img:hover,
.trust-strip--on-dark .trust-mq__item img:hover,
.partner-mq__item img:hover,
.se-vis--plate:hover img,
.case-mark:hover img {
  filter: none;
  opacity: 1;
}

/* ---------------------------------------------------------------------
   10. A POSITIONAL HIGHLIGHT THAT ONLY MEANT SOMETHING ON ONE PAGE.

   `.kan-col:nth-child(4) .kk-val { color: teal }` tinted whatever
   happened to be the fourth column. That reads as "done" on
   /services/hubspot-enablement/ (New / Working / Qualified / WON) and
   as a rendering bug everywhere else: on /expertise/enablement-
   processes/ the four columns are four peer playbooks, and on
   /services/fractional-marketing-lead/ the fourth column is "Parked",
   where a success colour is actively wrong. The highlight is now
   opt-in, carried by the column that means it.
   --------------------------------------------------------------------- */
.kan-col:nth-child(4) .kan-card .kk-val { color: var(--ui-ink); }
/* the opt-in must out-specify the positional reset above it (:nth-child
   counts as a class), hence the doubled class. */
.kan-col.kan-col--good .kan-card .kk-val { color: var(--color-teal); }

/* =====================================================================
   11. ALIGNMENT — TWO THINGS THAT MUST MATCH THEIR COUNTERPART.

   Both defects below survived every per-element check the last sweep
   ran. Neither element overflows, overlaps, clips or fails contrast;
   each one is fine on its own. They are only wrong NEXT TO the thing
   they are supposed to line up with, which is the one question a
   per-element check never asks.
   ===================================================================== */

/* ---------------------------------------------------------------------
   11a. A CAPTION BESIDE A CTA SHARES THE BUTTON'S BASELINE.

   "Free, and I read every one myself." sits beside "Start the HubSpot
   Quick Audit" in a .cta-row. .cta-row is a flex row set to
   `align-items: stretch` (v2.css item 7, so a button pair cannot come
   out at two different heights). Stretch is right for the buttons and
   wrong for a text caption: the caption's box was stretched to the
   button's full 56px and the text then painted at the TOP of that box,
   so measured in the browser the caption's baseline sat 18.5px above
   the button label's. Two lines of type, 18.5px apart, side by side.

   Measure the button's baseline with care. A .btn is itself a flex
   container with align-items:center, so a probe appended to it is
   CENTRED rather than baseline-aligned and reports the button's middle,
   not its text baseline: that reads the error as 12.0px instead of the
   true 18.5px. The number above comes from wrapping the button's own
   text node in an inline box and probing inside it.

   Every .cta-row on the site was swept at 1440/1280/768/390: this is
   the only row anywhere that pairs a button with a text caption on the
   same line. The other 275 button-plus-text groupings are STACKED
   (.promo__note, .form-note, .hint, the badge pairs), where left edges
   are what have to agree, and all 275 measured 0.0px of left-edge
   error. So the rule below is written to the pattern rather than to the
   page, but it currently binds to exactly one element.

   A row carrying a caption aligns on the BASELINE instead, so the
   caption's type and the button's label sit on one line of writing.
   The gap opens from 16px to 20px: 16px is the gap between two buttons,
   and a caption needs to read as an aside to the button rather than as
   a second control crowding it.
   --------------------------------------------------------------------- */
.cta-row:has(> .cta-note) {
  align-items: baseline;
  column-gap: var(--space-5);
  row-gap: 10px;
}
.cta-note {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
  max-width: 42ch;
}
.section--dark .cta-note,
.hero--dark .cta-note { color: var(--color-on-dark-soft); }
/* Below 640px the row wraps and the caption becomes its own flex line,
   where baseline alignment has nothing to align against. Left edges are
   what agree there, and they already do. */

/* ---------------------------------------------------------------------
   11b. THE TWO PROOF BADGES SHARE ONE AXIS, WRAPPED OR NOT.

   "Top HubSpot Partner Consultant 2024" wraps to two lines; "HubSpot
   Solution Partner" does not. v2.css item 1 reserved two lines of label
   height on BOTH pills (min-height: 2.7em) so the pair could not come
   out at two different heights, and that worked: measured on all 53
   badge groups on the site the cards are 72px and 72px (64px in the
   article sidebar), and both badge images are centred on the card.

   But reserving the height is not the same as USING it. The label is a
   block, so the one-line label rendered on the FIRST of its two
   reserved lines and left the second one empty. Measured: the wrapped
   label's text block centred at 35.57px against a 36.0px card centre
   and a 36.0px image centre, while the one-line label centred at
   27.15px, 8.4px higher than its neighbour, with 15px of dead space
   beneath it and its own badge art sitting 9px below it. Equal boxes,
   centred art, and the type still refusing to line up.

   `align-content: center` centres the label block inside the height
   that is already reserved for it, so a one-line label and a two-line
   label both centre on the card's own axis, which is the axis the badge
   image is on. The reservation stays exactly as it was, so the equal
   heights it buys are untouched; if a label ever runs to three lines it
   overflows the reservation symmetrically and .ratings{align-items:
   stretch} still carries the pair to a common height.
   --------------------------------------------------------------------- */
.rating-card__meta { display: grid; align-content: center; }

/* ==========================================================================
   COUNTERPART PARITY
   Two things that play the same role must measure the same. Everything below
   was found by grouping elements by ROLE across all 47 pages and diffing each
   instance against its group, not by checking any element on its own: every
   one of these passes overflow, contrast, size and geometry in isolation and
   is only wrong next to the thing it is supposed to match. Each is written as
   ONE rule covering BOTH sides of the pair, so the two cannot drift apart
   again the way they did.
   ========================================================================== */

/* --- 1. THE NARROW PROSE COLUMN HAD TWO WIDTHS -----------------------------
   `.container--narrow` is the site's reading column, 820px via
   --container-narrow. Inside a hero it silently became 880px, because
   `.page-hero .container{max-width:880px}` (0,0,2,0) out-specifies
   `.container--narrow{max-width:var(--container-narrow)}` (0,0,1,0). The class
   was written on those heroes deliberately and had no effect.

   MEASURED at 1280 on 23 pages (20 blogs, 2 events, /privacy/, /terms/):
   hero copy started at x=224 and the reading column below it started at
   x=254. On /privacy/ and /terms/ that is an <h1> and the very body text it
   introduces, stacked with nothing between them, disagreeing by 30px. Both
   columns are perfectly legible; only the pair is wrong.

   Converged on 820px: it is the declared token, it is what the explicit
   modifier asks for, and it is what 34 pages already render. The wide hero
   (.page-hero--wide, no --narrow) is untouched and stays at --container. */
.page-hero .container--narrow { max-width: var(--container-narrow); }

/* --- 2. THE TWO LOGO STRIPS SAT ON DIFFERENT RHYTHMS -----------------------
   .trust-strip (clients, 43 pages) and .partner-strip (technology partners,
   45 pages) are the same component in every structural respect: a full-width
   band, a centred uppercase label, a looping row of grey marks. A previous
   pass unified their label typography and their mark treatment. Their BAND
   never got unified, and 30 pages carry both, one after the other.

   MEASURED: trust 40px top and bottom, partner 48px, at 1280; 28px against
   32px at 390. Two bands, same job, adjacent on the page, 8px apart at
   desktop and 4px apart at mobile.

   Converged on the trust-strip value, which is the deliberate one: its clamp
   was authored specifically to override the older flat token, whereas the
   partner strip still carried the generic var(--space-12). One declaration
   now feeds both, at both breakpoints. */
.trust-strip:not(.trust-strip--on-dark),
.partner-strip { padding-block: clamp(28px, 3.4vw, 40px); }

/* ==========================================================================
   G23. FOUNDER PORTRAIT SLOT - /about/ hero
   "Sushil photo placeholder is missing in about section"

   THE PAGE IS ABOUT A PERSON AND HAD NO PLACE TO PUT HIM. Every other hero
   on the site is a .page-hero--wide .split with a visual in the right column;
   /about/ was the one narrow, single-column hero, on the one page whose
   subject is the founder. The slot is that column.

   It reserves the real photograph's box exactly - 4:5, the same border,
   radius and cream band the rest of the page already uses - so the layout
   does not move on the day the photograph is dropped in. It is NOT a broken
   image and it is NOT a stock face: no photograph of him exists in this
   repository and none may be fabricated, so the plate is authored as a plate.
   The monogram disc is the .tq__avatar idea at portrait scale, inverted
   (white disc on cream, rather than cream disc on white) because a cream disc
   on a cream band would disappear. See src/partials/founder-portrait.html for
   the one-line swap.
   ========================================================================== */
.portrait { margin: 0; width: 100%; max-width: 400px; margin-inline: auto; }

.portrait__frame {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  /* the trust strip's cream band, lit from where a portrait light would be,
     plus the inset hairline the cards on this page all carry */
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.72);
  background:
    radial-gradient(112% 78% at 50% 14%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0) 62%),
    linear-gradient(158deg, #FFFFFF 0%, #FFF7ED 58%, #FEF3C7 100%);
}
/* One cell, every occupant: the ring and the monogram today, the photograph
   after the swap. Same grid area, so the swap cannot shift anything, and the
   photograph covers the plate rather than sitting on it. */
.portrait__frame::before,
.portrait__mark,
.portrait__img { grid-area: 1 / 1; }
/* A single concentric hairline behind the monogram. Without it the disc is an
   avatar floating in a field; with it the plate has a centre and reads as
   composed - the same reason a portrait is shot to a frame and not to the
   middle of a wall. Struck in the border grey the cards already use, so it
   belongs to the page rather than decorating it. ONE ring, not two: two put a
   bullseye in the frame, which is a different picture. */
.portrait__frame::before {
  content: "";
  width: 72%; aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  opacity: 0.8;
  pointer-events: none;
}
/* The photograph replaces the plate outright - ring included. */
.portrait__frame:has(.portrait__img)::before { display: none; }
.portrait__img { display: block; width: 100%; height: 100%; object-fit: cover; }

.portrait__mark {
  display: grid; place-items: center;
  width: clamp(124px, 16vw, 184px); aspect-ratio: 1;
  border-radius: 50%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: 0 14px 34px rgba(17, 24, 39, 0.10);
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: clamp(2.3rem, 3.2vw, 3.15rem);
  line-height: 1;
  /* tracking pads the RIGHT of the second S only, which throws a two-letter
     monogram off centre inside a circle; the indent pays it back */
  letter-spacing: 0.07em;
  text-indent: 0.07em;
  color: var(--color-primary);
}

.portrait__cap {
  margin-top: var(--space-4);
  text-align: center;
  font-size: var(--fs-sm);
  line-height: 1.4;
  color: var(--color-text-muted);
}
.portrait__cap strong { color: var(--color-text); font-weight: 700; }

/* Below 820px the .split is one column and the hero reads top to bottom, so
   the plate follows the copy at a size that does not push the CTAs off the
   first screen. */
@media (max-width: 820px) {
  .portrait { max-width: 300px; margin-top: var(--space-8); }
}

/* ==========================================================================
   FEEDBACK ITEM 3 + 16 — THE v2 PROOF BADGES, WITH THE RATING BACK
   ==========================================================================
   "Bring back Top HubSpot Partner and HubSpot Solution Partner from v2 with
   stars. I prefer v2's rating, stars and number of reviews."

   v3 had stripped the card down to a single label line, so the pair proved a
   credential but no longer proved a SCORE. The score, the star row and the
   review count come back from v2 verbatim (see src/partials/proof-badges.html);
   this block is only the geometry that carries three rows instead of one.

   THE ONE THING THAT MUST NOT REGRESS: his earlier note, "both should be of
   same size across the website." v3 bought that with a two-line reservation on
   the single label (v2.css ITEM 1 + figures.css .rating-card__meta grid). A
   three-row card cannot be equalised line by line without stacking three
   separate reservations and a very tall pill, so the guarantee is moved UP one
   level: the CARD carries the min-height, the label block centres inside it,
   and .ratings{align-items:stretch} still carries any overflow to a common
   height. Same promise, one rule instead of three, and it holds in the stacked
   article sidebar where .ratings is a column and stretch buys nothing.
   -------------------------------------------------------------------------- */

/* Three rows need a real column, not inline spans that reflow into a
   paragraph. justify-content centres the block on the card's own axis, which
   is the axis the badge image is already on. */
.rating-card > span {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
}

/* Score and stars share a baseline: the numeral is 1.5rem and the stars 0.9rem,
   so aligning the boxes would sit the row visibly high. */
.rating-card__rate { display: flex; align-items: baseline; gap: 7px; }

/* The reservation is retired here, not deleted upstream: the card min-height
   below is what equalises the pair now. Left in place it would add a line of
   dead space under every label. */
.rating-card__meta {
  display: block;
  min-height: 0;
  line-height: 1.3;
  color: var(--color-text);
  font-weight: 600;
}

.rating-card__reviews {
  display: block;
  font-size: var(--fs-xs);
  line-height: 1.3;
  color: var(--color-text-muted);
}

/* 340px, browser-measured: the longer count, "10 reviews in the HubSpot
   ecosystem", asks for 226px of text box. At the old 288px pill it had 196px
   and wrapped; at 320px it had 228px, which fits by two pixels and is not a
   margin worth shipping. 340px gives it 248px. Two pills plus the 14px gap is
   694px, so the pair still sits on one row inside the 768px container, and the
   520px rule already in v2.css still takes them full width below that. */
.ratings { --pill-w: 340px; }
.rating-card { min-height: 96px; }

/* THE DARK HERO KEEPS THE LIGHT PILL, DELIBERATELY. Item 3 moves this pair
   into the homepage hero, which is .hero--dark. The translucent-plate route
   (rgba(255,255,255,.07), the treatment v2 used on its dark sections) was
   built and looked at first: both marks are supplied as hexagons with a dark
   navy outline and a white fill, so on a #0f172a plate the outline drops into
   the background and the badge reads as a grey smudge at 40px. These are two
   vendors' certification marks, not decoration, and the reference he pointed
   at (fb-1, lower) is the v2 pair on plain white. So the card stays on the
   light surface it uses on the other 45 pages and takes a lifted shadow to
   sit on the dark band. Every colour inside it is already stated absolutely
   (score/meta -> --color-text, reviews -> --color-text-muted) rather than
   inherited, so .hero--dark's on-dark body colour cannot leak in. */
.hero--dark .rating-card { box-shadow: 0 14px 34px rgba(0, 0, 0, 0.30); }
.hero--dark .rating-card:hover { box-shadow: 0 18px 42px rgba(0, 0, 0, 0.38); }

/* THE HERO PAIR IS A GRID, NOT A WRAPPING FLEX ROW. The hero's left column is
   595px at 1440, so two 340px pills do not fit and flex wraps them onto two
   lines. align-items:stretch only equalises WITHIN a flex line, so the moment
   they wrap the two cards size independently and the pair comes out 107px
   against 96px, which is the exact mismatch his earlier "both should be of
   same size" note was about. A two-track grid cannot wrap: both cards take the
   same track width and the same row height at every width the hero column
   takes, and auto-fit drops them to one full-width card only when the column
   is genuinely too narrow for two. */
.ratings--hero {
  margin-top: var(--space-6);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
  align-items: stretch;
}
.ratings--hero .rating-card { max-width: none; }
