/* ================================================================
   CAPABILITIES SHOWCASE — SPINE LAYOUT
   ================================================================

   Structure and type for the eleven spine screens. Colors come from
   the role layer in tokens.css — nothing here sets a fixed color.

   Layout vocabulary (SPINE_MAP_HYG_V1.md):
     .lay-t   text only
     .lay-tr  text left  / visual right
     .lay-tl  visual left / text right
     .lay-fb  full bleed
     .lay-2c  two-column comparative

   Rhythm rule: consecutive screens never share a layout code.
   Surface rule: consecutive screens never share a surface, and every
   screen declares data-surface explicitly.
   ================================================================ */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--spine-dark-flat);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--line-height-base);
  color: var(--fg);
  font-variation-settings: 'opsz' 16;
}

img { max-width: 100%; display: block; }
a   { color: inherit; }

/* Respect the reduced-motion preference globally, then let individual
   components opt back in where a static state would be worse. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* ── Screens ──────────────────────────────────────────────────────
   Every screen is at least one viewport so short content doesn't look
   dead, and grows past it when content demands. The keyboard module's
   smart-advance handles the taller-than-viewport case. */

/* TWO BACKGROUNDS, and only two. A screen's ground follows from its
   data-surface, so no screen carries its own background any more — that is what
   was making colours pop against each other down the scroll (Matt, 2026-07-27).
   Screens that legitimately carry their own ground (the cascade stage, the
   case-study photograph) set it on an inner element, not on .screen. */
.screen[data-surface="dark"]  { background: var(--spine-dark); }
.screen[data-surface="light"] { background: var(--spine-light); }

.screen {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(48px, 7vh, 100px);
  padding-inline: clamp(24px, 5vw, 64px);
}

/* Full-bleed: the VISUAL runs edge to edge. Text inside still needs a gutter,
   so the padding moves to .screen__inner rather than disappearing — without
   this, a knockout on a full-bleed screen runs off the viewport at mobile. */
.screen--flush { padding-inline: 0; }
.screen--flush > .screen__inner { padding-inline: clamp(24px, 5vw, 64px); }

.screen--auto  { min-height: 0; }       /* opts out of the viewport floor */

.screen__inner {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  /* Same reasoning as .brand-stream: these are flex items in a column, so their
     automatic minimum size is their content size. Wide content (a long
     unbroken string, a max-content track) would otherwise widen the page. */
  min-width: 0;
}

/* Grid children need the same guard: min-width:auto applies to grid items too,
   so a wide child can blow out its column instead of overflowing inside it. */
.screen__text,
.screen__visual { min-width: 0; }

/* Hairline between screens, suppressed on the first. */
.screen + .screen::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 1px;
  background: var(--rule-c);
  opacity: 0.5;
}

/* ── Layout codes ─────────────────────────────────────────────── */

.lay-tr .screen__inner,
.lay-tl .screen__inner,
.lay-2c .screen__inner {
  display: grid;
  gap: clamp(32px, 5vw, 56px);
  align-items: start;
}

/* 6/6 by default rather than 5/7. The design system sanctions either, but a
   display-tier title in a 5fr column breaks to four lines at 1440 and reads
   cramped. */
.lay-tr .screen__inner { grid-template-columns: 6fr 6fr; }
.lay-tl .screen__inner { grid-template-columns: 6fr 6fr; }

/* Content-led variant, the system's other sanctioned split. For sub-beat
   screens whose copy is deliberately one line (screens 7 and 8, where the
   language is locked): at 6/6 the text column reads as an empty half. Giving
   the content the larger share fixes the imbalance without touching copy. */
.lay-tr.is-content-led .screen__inner { grid-template-columns: 5fr 7fr; }
.lay-tl.is-content-led .screen__inner { grid-template-columns: 7fr 5fr; }
.lay-2c .screen__inner { grid-template-columns: 1fr 1fr; }

/* TL is the mirror of TR: the visual is authored second in the DOM for
   reading order, then placed first visually. */
.lay-tl .screen__text   { order: 2; }
.lay-tl .screen__visual { order: 1; }

@media (max-width: 900px) {
  .lay-tr .screen__inner,
  .lay-tl .screen__inner,
  .lay-2c .screen__inner { grid-template-columns: 1fr; }
  .lay-tl .screen__text   { order: 1; }
  .lay-tl .screen__visual { order: 2; }
}

/* ── Type ─────────────────────────────────────────────────────────
   Two title tiers. The display tier is for a screen that OPENS a
   section; sub-beats stay on the beat tier. Trigger is section-opener,
   not "numbered beat" — the Showcase renders no numbers. */

.eyebrow {
  font-size: var(--text-eyebrow);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--eyebrow);
  margin-bottom: var(--space-lg);
}

/* No middot before an eyebrow. The design system's separator exists to
   divide a number from a label; with no number there is nothing to
   divide. A middot appears only when an eyebrow genuinely carries two
   elements, via .eyebrow__sep. */
.eyebrow__sep::before { content: " · "; opacity: 0.55; }

.title {
  font-size: var(--text-display);
  font-weight: var(--weight-light);
  line-height: 1.08;
  letter-spacing: var(--tracking-display);
  font-variation-settings: 'opsz' var(--opsz-display);
  text-wrap: balance;
}

[data-tier="display"] .title {
  font-size: var(--text-display-lg);
  line-height: 1.03;
  letter-spacing: var(--tracking-display-lg);
}

/* The bold run gets its tracking back at the display tier too. */
[data-tier="display"] .title em {
  letter-spacing: calc(var(--tracking-display-lg) + var(--tracking-em-adjust));
}

.body { max-width: var(--measure-text); margin-top: var(--space-lg); }
.body p { color: var(--fg-soft); }
.body p + p { margin-top: var(--space-md); }

/* The opening paragraph of a screen is a lede by default. Opt out with
   .body--no-lede when a screen opens on ordinary copy. */
.body > p:first-child {
  font-size: var(--text-lede);
  line-height: 1.5;
  color: var(--fg);
}
.body--no-lede > p:first-child {
  font-size: var(--text-base);
  line-height: var(--line-height-base);
  color: var(--fg-soft);
}

.quiet { color: var(--fg-mute); font-size: var(--text-base); }
.title + .quiet,
.title + .body { margin-top: var(--space-lg); }
.eyebrow + .quiet { margin-top: 0; }

/* ── CTAs — the teaser/branch contract ────────────────────────────
   Chevron direction is the ONLY differentiator:
     .cta--branch  →  another page
     .cta--sub     ↓  the next screens of this scroll
   Shape, padding, and surface-reactive color are identical so the
   pattern is learnable once. */

.cta {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  margin-top: var(--space-xl);
  padding: 12px 28px;
  border: 1px solid color-mix(in srgb, var(--em) 45%, transparent);
  border-radius: var(--radius);
  background: transparent;
  color: var(--em);
  font-family: inherit;
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: 1.8px;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}

.cta:hover,
.cta:focus-visible {
  background: color-mix(in srgb, var(--em) 14%, transparent);
  border-color: var(--em);
}

.cta__chev { transition: transform var(--dur-fast) var(--ease); }
.cta--branch:hover .cta__chev { transform: translateX(3px); }
.cta--sub:hover    .cta__chev { transform: translateY(3px); }

@media (prefers-reduced-motion: reduce) {
  .cta__chev { transition: none; }
  .cta--branch:hover .cta__chev,
  .cta--sub:hover    .cta__chev { transform: none; }
}

/* Focus ring — amber, the one sanctioned exception use. */
:focus-visible {
  outline: 2px solid var(--accent-2);
  outline-offset: 2px;
}

/* ── Nav ──────────────────────────────────────────────────────── */

.nav {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: 100;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: clamp(16px, 4vw, 32px);
  background: var(--nav-bg);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--nav-border);
  transition: background var(--dur-base) var(--ease);
}

.nav.is-scrolled { background: var(--nav-bg-scrolled); }

/* The wordmark inherits this colour via fill="currentColor". The nav bar is
   light, so the wordmark is navy — V1's onLight branch. */
.nav__logo { display: flex; align-items: center; color: var(--ink); }
.nav__wordmark { display: block; height: 22px; width: auto; }

.nav__toggle {
  display: grid;
  gap: 6px;
  padding: 8px;
  background: none;
  border: 0;
  cursor: pointer;
}
.nav__toggle span {
  display: block;
  width: 24px;
  height: 1.5px;
  background: var(--ink);
  transition: transform var(--dur-base) var(--ease),
              opacity var(--dur-base) var(--ease);
}
.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7.5px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7.5px) rotate(-45deg); }

/* Flyout. Two zones: section links for everyone, and a presenter zone
   rendered only for an authenticated session (UX_SPEC.md §5.4.1).
   The presenter zone is absent from the DOM for a prospect, not hidden
   — it is injected by lib/presenter-tools.js when a session resolves. */
.flyout {
  position: fixed;
  inset: var(--nav-h) 0 0;
  z-index: 99;
  background: var(--scrim);
}
.flyout[hidden] { display: none; }

/* TWO COLUMNS — page navigation, then everything else.

   Matt, 2026-07-29: the flyout "has become a bit too big… I think this needs to be
   expanded horizontally into two columns. First column is page navigation, second
   column is everything else."

   Flex rather than an explicit two-column grid, and that is the load-bearing
   choice: a prospect has NO presenter zone in their DOM — lib/presenter-tools.js
   only injects it once a session resolves — so a declared second grid column would
   leave them a visible empty half. With flex the panel simply has one child and
   reads as one column. The second column appears because something is in it.

   The panel also gets wider here: it was capped at --measure, a reading measure
   sized for prose, which is what made six links plus a presenter zone stack into
   something so tall. */
.flyout__panel {
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  padding: var(--space-lg) clamp(16px, 4vw, 32px) var(--space-xl);
  /* fit-content, not a fixed width. The flex ROW hugs its children, but the panel
     around it does not — so with only the nav column present a prospect got a
     1100px-wide white box with content in the left quarter and nothing in the rest.
     Hugging means one column reads as a narrow panel and two columns widen it,
     which is the behaviour the flex choice above was for. */
  width: fit-content;
  max-width: min(1100px, 94vw);
  margin-inline: auto;
  display: flex;
  align-items: flex-start;
  gap: clamp(24px, 5vw, 64px);
}
/* Column one. Takes its natural width and stops growing, so a long presenter zone
   does not squeeze the nav into two-line labels. */
.flyout__nav {
  flex: 0 1 clamp(200px, 26vw, 280px);
  min-width: 0;
}
/* Column two — whatever presenter-tools injected. */
.flyout__panel > .ptools {
  flex: 1 1 auto;
  min-width: 0;
  /* A rule between the columns, on the side that only exists when there are two. */
  border-left: 1px solid var(--rule);
  padding-left: clamp(20px, 3vw, 36px);
}
/* Below the two-column threshold, stack — and the rule moves to the top edge,
   since a left border on a stacked block reads as an indent. */
@media (max-width: 780px) {
  .flyout__panel { flex-direction: column; gap: var(--space-lg); width: auto; }
  .flyout__nav { flex: 1 1 auto; width: 100%; }
  .flyout__panel > .ptools {
    border-left: 0;
    border-top: 1px solid var(--rule);
    padding-left: 0;
    padding-top: var(--space-lg);
    width: 100%;
  }
}

/* Tightened with the two-column change: 18px of padding on an 18px face made a
   six-item list nearly 300px tall on its own, which was most of "too big". */
.flyout__link {
  display: block;
  padding: 13px 0;
  font-size: 17px;
  font-weight: var(--weight-light);
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  transition: color var(--dur-fast) var(--ease);
}
.flyout__link:hover { color: var(--accent); }

/* ── Screen 1 · the cascade ───────────────────────────────────────
   Full-bleed dark stage. Phrases are absolutely centred and animated
   by two keyframe sets generated at runtime by lib/hook-cascade.js,
   because their percentages depend on phrase count and pace. */

.cascade {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  /* Floor as well as a ceiling. 16:9 alone collapses to ~219px at 390px wide,
     which leaves a display-size phrase nowhere to go — V1 had the same problem
     and never fixed it. The floor wins at narrow widths, the ratio at wide. */
  min-height: min(62vh, 520px);
  max-height: 78vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  /* This gradient is now the whole spine's dark ground, so it lives in
     --spine-dark and the stage just references it. Screen 1 is where it came
     from; every other dark screen inherits it. */
  background: var(--spine-dark);
}

/* Phrase type steps down on a phone so a long phrase doesn't fill the stage. */
@media (max-width: 600px) {
  .cascade__phrase { font-size: clamp(1.9rem, 8.5vw, 2.6rem); }
}

.cascade__phrase {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  max-width: 1080px;
  padding-inline: clamp(24px, 5vw, 48px);
  text-align: center;
  font-size: var(--text-cascade);
  font-weight: var(--weight-light);
  line-height: 1.08;
  letter-spacing: var(--tracking-display-lg);
  font-variation-settings: 'opsz' var(--opsz-display);
  color: var(--fg);
  opacity: 0;
  will-change: opacity, transform;
}

.cascade__phrase em {
  letter-spacing: calc(var(--tracking-display-lg) + var(--tracking-em-adjust));
}

/* Reduced motion: no cascade. Pin the final phrase visible so the
   screen still says something. */
@media (prefers-reduced-motion: reduce) {
  .cascade__phrase { animation: none !important; opacity: 0; }
  .cascade__phrase--final { opacity: 1; }
}

.cascade-scroll-cue {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin-top: var(--space-xl);
}
.cascade-scroll-cue__line {
  width: 1px;
  height: 40px;
  background: linear-gradient(180deg, var(--em), transparent);
}
.cascade-scroll-cue__label {
  font-size: 10px;
  letter-spacing: 1.5px;
  color: var(--fg-faint);
  text-transform: uppercase;
}

/* ── Utility ──────────────────────────────────────────────────── */

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ── Footer ───────────────────────────────────────────────────────
   Chrome, not a spine screen. Deliberately excluded from .screen so it
   doesn't enter the surface-alternation count or become a keyboard stop. */

.site-footer {
  background: var(--footer-bg);
  border-top: 1px solid var(--footer-rule);
  padding: 36px clamp(24px, 5vw, 48px);
}
.site-footer .screen__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

/* ── Screen 3 · Who We Are — RTB tiles ────────────────────────────
   auto-fit so the grid composes at 4 tiles and at 11 without a hardcoded
   row count. A curator chooses how many appear, so the layout can't assume. */

.rtb-grid {
  list-style: none;
  display: grid;
  /* min() so the minimum track can never exceed the container. Plain
     minmax(150px, 1fr) refuses to shrink below 150px and pushes the page
     sideways at narrow widths. */
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  gap: var(--space-lg) var(--space-md);
}

.rtb {
  min-width: 0;
  overflow-wrap: break-word;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--rule-c);
}

.rtb__number {
  font-size: clamp(28px, 3.2vw, 40px);
  font-weight: var(--weight-light);
  line-height: 1;
  letter-spacing: var(--tracking-display);
  color: var(--em);
  font-variation-settings: 'opsz' var(--opsz-display);
}

.rtb__label {
  font-size: 13px;
  line-height: 1.4;
  color: var(--fg-soft);
}

/* A principle has no number, so it leads with its label at stat weight to keep
   the grid's optical rhythm. */
.rtb--principle .rtb__label {
  font-size: 15px;
  color: var(--fg);
}

/* ── Screen 3 · the three accreditations ──────────────────────────
   Was three uppercase text chips in a hairline box. Now the real marks, which
   is what Matt asked for on 30 Jul and what the certifications are actually
   entitled to look like.

   Optical sizing, NOT equal heights. The two landscape marks are ~2.4:1 and the
   B Corp mark is PORTRAIT at 0.68:1, so setting one shared height would leave
   B Corp about a quarter the visual weight of its neighbours. Its height is
   raised instead until the three read as equals — the usual way a logo row is
   balanced, and the reason there is no single --badge-height token here. */
.trust-badges {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-lg);
  margin-top: var(--space-xl);
}
.trust-badge {
  display: block;
}
/* The two monochrome marks carry no fill of their own, so the mask paints them
   in the surface's foreground colour: white here, navy on a light surface, with
   no second rule and no white-only duplicate file to keep in sync. */
.trust-badge--employee,
.trust-badge--bcorp {
  background: var(--fg);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: left center;
          mask-position: left center;
  -webkit-mask-size: contain;
          mask-size: contain;
}
.trust-badge--employee {
  width: 104px;  /* 2.36:1 */
  height: 44px;
  -webkit-mask-image: url("../assets/badge-employee-owned.svg");
          mask-image: url("../assets/badge-employee-owned.svg");
}
.trust-badge--bcorp {
  /* 0.68:1, and its lockup puts small type above and below the B, so it needs
     more height than the wide marks to carry the same weight. 44/64 measured by
     eye against the render at 44px — 58px still read as the junior partner. */
  width: 44px;
  height: 64px;
  -webkit-mask-image: url("../assets/badge-b-corp.svg");
          mask-image: url("../assets/badge-b-corp.svg");
}
/* Full colour, so a real image. Height only — width follows the intrinsic
   ratio, which keeps the WBENC lockup's proportions exactly as licensed. */
.trust-badge--women {
  height: 44px;
  width: auto;
}

/* ── Screen 4 · Brands that trust us — the stream ─────────────────
   The track holds the list twice and translates left by exactly half its
   width, so the wrap point lands where the duplicate begins and reads as
   continuous. Duration is set per-render by lib/brand-stream.js from the item
   count, so apparent speed stays constant at 3 brands or 20. */

/* The white band the marquee runs in. Full-bleed — the screen is .screen--flush
   precisely so this element can reach both edges.

   Also where the breathing room lives: Matt, 30 Jul, "Create a little more
   vertical room between the client testimonials and the infinite scroll." The gap
   used to be a single --space-xl margin on the stream; it is now the band's own
   margin above plus its internal padding, which reads as more air AND gives the
   logos room to be larger without touching the quotes.

   The gap was one --space-xl (48px). It is now 56 + 28 = 84px of clear air —
   "a little more", not a new screen. */
.brand-band {
  margin-top: var(--space-2xl);
  padding-block: var(--space-lg);
  background: var(--surface);
  /* The band is a grid item in .lay-fb and gets STRETCHED to fill its row, so it
     is routinely taller than the strip inside it — measured 188px around a 76px
     marquee. Without centring, that surplus all lands below the logos and the
     white band reads visibly bottom-heavy. */
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.brand-stream {
  position: relative;
  overflow: hidden;
  width: 100%;
  /* min-width: 0 is load-bearing. A flex item's automatic minimum size is its
     CONTENT size, and this track is `width: max-content` holding the list
     twice — so without this the item refuses to shrink below its content, the
     overflow:hidden never gets a chance, and the whole page scrolls sideways.
     Measured 504px of horizontal scroll at a 390px viewport before this line. */
  min-width: 0;
  /* Spacing moved to .brand-band above, which owns the gap now. */
  /* Fade the strip into the surface at both edges so items enter and leave
     rather than appearing to be clipped. */
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.brand-stream__track {
  display: flex;
  width: max-content;
  animation-name: hygBrandStream;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.brand-stream:hover .brand-stream__track,
.brand-stream:focus-within .brand-stream__track {
  animation-play-state: paused;
}

.brand-stream__set {
  list-style: none;
  display: flex;
  align-items: center;
  /* Study B's spacing: 16px of clearance either side of a mark, so 32px between
     two adjacent ones. 2.22vw resolves to exactly 32px at a 1440 viewport, which
     is the width the study was judged at. */
  gap: clamp(16px, 2.22vw, 32px);
  padding-inline: clamp(16px, 2.5vw, 36px);
}

@keyframes hygBrandStream {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* EVERY logo gets the SAME BOX, and scales to fit inside it.
   This is the difference between a logo wall that looks composed and one that
   looks like a folder of files. Capping height alone makes a square mark
   (Blue Buffalo's diamond, the McDonald's arches) render 36x36 next to a
   wordmark at 130x20 — barely a third of the visual area, so it reads as an
   afterthought. With a fixed box plus object-fit: contain, a square fills the
   box's height and a wordmark fills its width, and the two carry roughly equal
   optical weight. */
/* ── 4 AUG · THE FIXED BOX IS GONE ────────────────────────────────
   Replaced by three heights, one per shape, set from each logo's own measured
   aspect ratio (lib/brand-stream.js adds data-shape on load). Approved as
   "Study B" from docs/reference/logo-margin-study.html.

   Why the box had to go: one 194x76 frame at 2.56:1 with object-fit: contain
   meant the file's proportions decided its drawn size, and both extremes lost.
   Mark Anthony at 1.0 filled 39% of the box; Biofreeze at 6.2 filled 41%; only
   marks near 2.56:1 filled it. Measured spread across the set was 3.40x even
   after every file was trimmed. Under this system it is 1.84x.

   The heights ARE Study B's numbers — 66 / 46 / 32 at a 1440 viewport — wrapped
   in clamp() so all three scale by the same factor on smaller screens and the
   relationship between buckets holds. Widths are no longer set at all: each mark
   is as wide as its own artwork at that height, which is what makes margins
   measurable from the logo's edge rather than a frame's. */
.brand {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Pre-load height only — data-shape overrides it the moment the image decodes.
     Without this the row would collapse between markup and decode. */
  height: clamp(32px, 3.2vw, 46px);
}
/* Heights are set per mark by lib/brand-stream.js, from the measured height of
   each logo's WORDMARK rather than its bounding box — see the long note there for
   why the box was the wrong unit. CSS only holds the pre-load fallback above, so
   nothing collapses between markup and decode. data-shape is left on the element
   for inspection and future hooks, not for sizing. */
.brand[data-shape] { height: auto; }

/* Real logo assets vary wildly in aspect ratio and internal padding, so a single
   fixed height makes some tower over others. Cap BOTH axes and let object-fit
   letterbox inside the box, so every mark sits in a consistent footprint. */
.brand__logo {
  /* Height comes from the parent's shape bucket; width follows the artwork. No
     object-fit, because there is no longer a box to fit into — the mark IS the
     box. This is what lets side margins be measured from the logo's own edge. */
  height: 100%;
  width: auto;
  max-width: none;
  /* opacity stays 1: multiply already softens these against the cream, and
     stacking transparency on top washes out the lighter marks. */
  opacity: 1;
  /* 19 of the 141 files are JPG, which cannot hold transparency — each carries an
     opaque white field that would read as a pale rectangle on the cream ground.
     multiply drops white out while leaving the ink, which is the standard
     treatment for a logo wall. Deliberately NOT --icon-filter: these are client
     marks, and inverting or recolouring someone's logo is not ours to do. */
  mix-blend-mode: multiply;
}

/* On a dark surface the equivalent blend is screen, and the white variants are
   what get used. Screen 4 is light today; this is here so moving it doesn't
   silently break. */
[data-surface="dark"] .brand__logo {
  mix-blend-mode: screen;
  opacity: 1;
}

/* Text plate — what renders until logo assets exist (content debt #19). Set to
   read as a wordmark rather than as body copy, so a strip of these still looks
   deliberate. */
.brand__name {
  font-size: clamp(15px, 1.5vw, 19px);
  font-weight: var(--weight-medium);
  letter-spacing: -0.01em;
  color: var(--fg-soft);
  white-space: nowrap;
}

/* Reduced motion: no scroll. Becomes a centred wrapped row, and the duplicate
   set is removed from the flow so nothing reads twice. */
@media (prefers-reduced-motion: reduce) {
  .brand-stream__track { animation: none; width: 100%; flex-wrap: wrap; justify-content: center; }
  .brand-stream__set[aria-hidden="true"] { display: none; }
  .brand-stream__set { flex-wrap: wrap; justify-content: center; row-gap: var(--space-lg); }
  .brand-stream { mask-image: none; -webkit-mask-image: none; }
}

/* ── Screen 5 · What We Do — two comparative columns ──────────────
   Retires V1's Capabilities/Areas toggle. Both sets are on screen at once.
   The header spans both columns; the lists sit beneath it. */

.lay-2c .screen__head {
  grid-column: 1 / -1;
  margin-bottom: var(--space-lg);
}

.col__heading {
  font-size: var(--text-eyebrow);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--eyebrow);
  padding-bottom: var(--space-xs);
  border-bottom: 1px solid var(--rule-c);
  margin-bottom: var(--space-md);
}

.cap-list { list-style: none; display: flex; flex-direction: column; }

.cap {
  padding-block: var(--space-md);
  border-bottom: 1px solid var(--rule-c);
}
.cap:last-child { border-bottom: 0; }

/* 30 JUL · 20px -> 23px. Matt, on both columns: "find a way to bump up the white
   font size (e.g., Insight, Strategy, Innovation) - these should all be bumped up
   a little bit." One rule covers Capabilities and Areas of Expertise, because
   both columns are the same .cap component. */
.cap__name {
  font-size: 23px;
  font-weight: var(--weight-regular);
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.cap__tagline {
  margin-top: 4px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--em);
}

.cap__verb {
  margin-top: var(--space-sm);
  font-size: 14px;
  line-height: 1.55;
  color: var(--fg-soft);
}

/* The offerings disclosure. A real <details>, so keyboard and screen-reader
   behavior come free — V1 hand-rolled this with maxHeight and scrollHeight. */
.cap__more { margin-top: var(--space-sm); }

.cap__summary {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  cursor: pointer;
  font-size: 11px;
  font-weight: var(--weight-medium);
  /* em, not px — house rule. Was 1.2px. */
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--fg-mute);
  list-style: none;
  transition: color var(--dur-fast) var(--ease);
}
.cap__summary::-webkit-details-marker { display: none; }
.cap__summary::after {
  content: "";
  width: 6px; height: 6px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(45deg) translateY(-1px);
  transition: transform var(--dur-fast) var(--ease);
}
.cap__more[open] .cap__summary::after { transform: rotate(-135deg) translateY(-1px); }
.cap__summary:hover { color: var(--em); }

.cap__offerings {
  margin-top: var(--space-sm);
  padding-left: 1.1em;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--fg-soft);
}

@media (prefers-reduced-motion: reduce) {
  .cap__summary, .cap__summary::after { transition: none; }
}

/* ── Wellness matrix keyframes ─────────────────────────────────────
   Lifted from the V1 bundle (index.html:14176-14205 at tag v1-spine).
   Keyframes only; all state is driven from JS via inline styles by
   lib/wellness-matrix.js.

   RESTORED 2026-07-29. This block lost its opening `@keyframes wcellPop {` and
   its first two stops somewhere in the port from the V1 bundle, leaving an
   orphaned `100% { … }` and a stray `}` — one extra closing brace in the whole
   file, which is how it was found.

   The consequence was invisible in the CSS and visible on screen:
   lib/wellness-matrix.js calls showCell() with `wcellPop 0.5s ease-out forwards`,
   and since no such keyframe existed the matrix cells never animated in. They
   simply appeared. That is the second half of "the teasers stopped looking
   animated" — the first half was the re-attach race fixed in lib/wellness-matrix.js.

   Stops recovered verbatim from docs/archive/index-v1-bundle.html:14177-14181. */
@keyframes wcellPop {
  0%   { opacity: 0; transform: scale(0.8); }
  60%  { opacity: 1; transform: scale(1.05); }
  100% { opacity: 1; transform: scale(1); }
}
@keyframes wsparkle {
  0%, 100% { opacity: 0; }
  50%      { opacity: 1; }
}
@keyframes wgridFlash {
  0%   { opacity: 0.06; }
  35%  { opacity: 1;    }
  70%  { opacity: 0.55; }
  100% { opacity: 0.38; }
}
.wcell-base { transform-origin: center; transform-box: fill-box; }

/* Brand Positioning interlocking rings — draw animation */
@keyframes bpRingDraw {
  to { stroke-dashoffset: 0; }
}
@keyframes bpRingFillFade {
  from { opacity: 0; }
  to   { opacity: 0.12; }
}
@keyframes bpRingLabelFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ── Screen 7 · Perspectives — the card carousel ──────────────────
   Every active perspective is mounted; the current one is visible and the rest
   are [hidden]. Not created and destroyed on cycle, because the BP rings SVG
   holds animation state that a remount would restart. */

.persp-stage { position: relative; }

.persp {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  /* No coloured top stripe. Removed on Matt's 2026-07-27 review. accentColor is
     still carried in the data and still used inside the generic fallback
     artwork; it is simply not drawn as a bar on the card any more. */
  padding: var(--space-lg);
}
.persp[hidden] { display: none; }

.persp__visual {
  /* The artwork sits on a light plate regardless of surface: these SVGs are
     drawn with brand marks in mind and were authored against a light ground.
     This is the one component that deliberately does not invert. */
  background: var(--surface-paper);
  padding: var(--space-md);
}
.persp__visual svg { width: 100%; height: auto; overflow: visible; display: block; }

.persp__name {
  font-size: 24px;
  font-weight: var(--weight-regular);
  line-height: 1.15;
  letter-spacing: -0.015em;
}

.persp__tagline {
  font-size: 15px;
  line-height: 1.55;
  color: var(--fg-soft);
  max-width: 46ch;
}

.persp__body .cta { margin-top: var(--space-sm); }

.persp-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-top: var(--space-md);
  flex-wrap: wrap;
}
.persp-nav__count {
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--fg-mute);
}
.persp-nav__next { margin-top: 0; }

/* ── Screen 8 · Models ────────────────────────────────────────────
   The dual-mode accordion is NOT ported. See lib/models-list.js. */

.model-list { list-style: none; display: flex; flex-direction: column; }

.model {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-lg);
  border-bottom: 1px solid var(--rule-c);
  flex-wrap: wrap;
}
.model:last-child { border-bottom: 0; }

.model__text { flex: 1 1 22ch; }

.model__name {
  font-size: 22px;
  font-weight: var(--weight-regular);
  line-height: 1.2;
  letter-spacing: -0.015em;
}

.model__tagline {
  margin-top: 6px;
  font-size: 15px;
  line-height: 1.55;
  color: var(--fg-soft);
  max-width: 52ch;
}

.model__link {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--em);
  text-decoration: none;
  white-space: nowrap;
  transition: opacity var(--dur-fast) var(--ease);
}
.model__link:hover { opacity: 0.72; }
.model__link:hover .cta__chev { transform: translateX(3px); }

@media (prefers-reduced-motion: reduce) {
  .model__link { transition: none; }
  .model__link:hover .cta__chev { transform: none; }
}

/* ── Screen 9 · Our Work — the case-study teaser ───────────────────
   THE GEOMETRY RULE, and the reason this block is commented so heavily:

   V1 absolutely positioned the card over the image with fixed top/bottom
   insets, which made card height viewport-derived. At short viewports the flex
   column overflowed and the CTA row spilled visually outside the panel
   (Design Debt #8, fixed June 2026 with three style values).

   V2 removes the cause. The card is a GRID ITEM IN FLOW layered over an
   absolutely positioned image, so its height is content-driven by construction
   and there is no fixed inset for the CTA row to escape through. The 640px
   stage floor is kept — that was about giving the photograph presence, not
   about containing the card.

   Verify any change here at 1440x940, 1180x620 and 390x844. Those are the
   three the V1 fix was signed off at, and they are the shape of the bug. */

/* Breathing room between the knockout title and the top edge of the
   photograph, which otherwise meet flush. */
.cs { margin-top: var(--space-xl); }

.cs__stage {
  position: relative;
  display: grid;
  /* The floor V1 introduced, kept. */
  min-height: max(72vh, 640px);
  align-content: end;
  padding: clamp(24px, 4vw, 56px);
  gap: var(--space-md);
  overflow: hidden;
}

.cs__image {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}

.cs__scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(4,0,32,0.15) 0%, rgba(4,0,32,0.55) 55%, rgba(4,0,32,0.85) 100%);
}

/* The card. In flow, so it cannot be overflowed by its own contents. */
.cs__card {
  position: relative;
  z-index: 1;
  justify-self: start;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  width: 100%;
  max-width: 540px;
  padding: clamp(20px, 2.4vw, 32px);
  background: var(--cs-accent, var(--spine-dark-flat));
  /* The card carries a client's palette, so its ink is fixed white rather than
     role-derived: it sits on a brand colour, not on a spine surface. This is a
     deliberate, contained exception. */
  color: #FFFFFF;
}

.cs__category {
  align-self: flex-start;
  font-size: 10px;
  font-weight: var(--weight-bold);
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: rgba(255,255,255,0.72);
  border: 1px solid rgba(255,255,255,0.35);
  padding: 4px 9px;
}

.cs__mast { display: flex; align-items: center; min-height: 44px; margin-block: var(--space-xs); }
.cs__logo { width: auto; max-width: 100%; object-fit: contain; }
.cs__client {
  font-size: 13px;
  font-weight: var(--weight-bold);
  letter-spacing: 2px;
  text-transform: uppercase;
}

.cs__eyebrow {
  font-size: 10px;
  font-weight: var(--weight-medium);
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--cs-accent-soft, rgba(255,255,255,0.6));
  opacity: 0.85;
}

.cs__headline {
  font-size: clamp(26px, 2.6vw, 36px);
  font-weight: var(--weight-light);
  line-height: 1.08;
  letter-spacing: var(--tracking-display);
}
.cs__headline-em {
  color: var(--cs-accent-soft, #FFFFFF);
  font-weight: var(--weight-em);
  font-style: normal;
  letter-spacing: calc(var(--tracking-display) + var(--tracking-em-adjust));
}

.cs__titleline {
  font-size: 15px;
  line-height: 1.4;
  color: rgba(255,255,255,0.9);
}

/* The only shrinkable region in the card. Everything else holds its size, so
   pressure lands here rather than on the CTA row. */
.cs__desc {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin-top: var(--space-xs);
  font-size: 14px;
  line-height: 1.6;
  color: rgba(255,255,255,0.82);
}

/* Never shrinks. The CTAs stay inside the card at every viewport. */
.cs__ctas {
  flex-shrink: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

.cs__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 11px 20px;
  border: 1px solid rgba(255,255,255,0.55);
  background: transparent;
  color: var(--cs-accent-soft, #FFFFFF);
  font-family: inherit;
  font-size: 10.5px;
  font-weight: var(--weight-medium);
  letter-spacing: 1.6px;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.cs__cta:hover { background: rgba(255,255,255,0.15); border-color: #FFFFFF; color: #FFFFFF; }
.cs__cta--secondary { border-color: rgba(255,255,255,0.35); color: rgba(255,255,255,0.85); }
.cs__cta:hover .cta__chev { transform: translateX(3px); }

.cs__tabs {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  justify-self: end;
}
.cs__tab {
  padding: 7px 14px;
  border: 1px solid rgba(255,255,255,0.35);
  background: transparent;
  color: rgba(255,255,255,0.75);
  font-family: inherit;
  font-size: 10px;
  font-weight: var(--weight-medium);
  letter-spacing: 1.4px;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.cs__tab:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.7); color: #FFFFFF; }
.cs__tab.is-active {
  background: #FFFFFF;
  border-color: #FFFFFF;
  color: var(--cs-accent-deep, var(--ink));
}

/* Mobile: the description clamps to three lines instead of compressing or
   scrolling against the CTA row. Pairs with .cs__desc above. */
@media (max-width: 700px) {
  .cs__desc {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
    flex: 0 0 auto;
  }
  .cs__card { max-width: 100%; }
  .cs__tabs { justify-self: start; }
}

@media (prefers-reduced-motion: reduce) {
  .cs__cta, .cs__tab { transition: none; }
  .cs__cta:hover .cta__chev { transform: none; }
}

/* ── Screen 10 · Meet Us — the team grid ───────────────────────────
   auto-fit, no assumed row count: has to compose at 4 members and at 40.
   On Default all forty are active, which makes this the tallest screen in the
   spine — see lib/team-grid.js for why that's deliberate. */

.member-grid {
  list-style: none;
  display: grid;
  /* min() so the minimum track can never exceed the container — this grid was
     measured causing 5px of horizontal page scroll at 390px without it. */
  grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr));
  gap: var(--space-lg) var(--space-md);
}

.member {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  min-width: 0;               /* grid items default to min-width:auto */
  overflow-wrap: break-word;  /* a long unbroken string can't widen the track */
}

.member__portrait {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
}

.member__photo { width: 100%; height: 100%; object-fit: cover; }

/* Initials fallback, so a card never shows an empty frame. */
.member__initials {
  font-size: clamp(22px, 2.4vw, 32px);
  font-weight: var(--weight-light);
  letter-spacing: 0.04em;
  color: var(--fg-mute);
}

.member__name {
  font-size: 17px;
  font-weight: var(--weight-regular);
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.member__title {
  margin-top: 2px;
  font-size: 12px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.02em;
  color: var(--em);
}

.member__bio {
  margin-top: var(--space-xs);
  font-size: 13px;
  line-height: 1.55;
  color: var(--fg-soft);
}

/* ── Screen 11 · the close ────────────────────────────────────────── */

/* 30 JUL · Now a COLUMN, not a row: the button, then the contact lines directly
   beneath it, left aligned, per Matt's note. align-items keeps the button hugging
   its own text instead of stretching the full column width. */
.close-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-lg);
  margin-top: var(--space-xl);
}
.close-actions .cta { margin-top: 0; }

/* The phone and email pair under the button.
   NOTE this partly reverses the V2 decision recorded under Footer below, which
   moved contact out of this screen and into the footer. Matt asked for it here on
   30 Jul, so it is here — and it now appears in BOTH places. Flagged for him
   rather than quietly stripped from the footer, which he did not ask for. */
.close-contact {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  font-size: 15px;
  line-height: 1.5;
}
.close-contact li {
  display: flex;
  align-items: center;
  gap: 0.6em;
}
.close-contact a {
  color: var(--fg-soft);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.close-contact a:hover {
  color: var(--fg);
  border-bottom-color: currentColor;
}
/* "Discreet little icons (same size as the phone number and email)" — so 1em,
   which tracks the type rather than a fixed pixel size. flex-shrink:0 stops them
   being squeezed when a long address wraps. */
.close-contact__icon {
  width: 1em;
  height: 1em;
  flex: 0 0 auto;
  color: var(--fg-mute);
}

/* ── Footer ────────────────────────────────────────────────────────
   Every footer line lightens from rgba(238,240,255,0.2) to
   rgba(238,240,255,0.62).

   HISTORY, because this moved twice. V1's queued change pulled email and phone OUT
   of the CTA screen and into the footer, which is what this comment used to
   describe. On 30 Jul the CTA gained a real contact block with icons, so the footer
   copy became a duplicate; on 4 Aug the footer line was removed (item E1) and the
   footer now carries only the copyright and the address. The CTA block is the one
   home for contact details. */

.site-footer__contact {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  align-items: baseline;
}

.site-footer a,
.site-footer span {
  font-size: 12px;
  color: var(--footer-fg);
  text-decoration: none;
}
.site-footer a:hover { color: var(--em); }

.site-footer__sep { opacity: 0.45; }

/* ── Presenter tools ───────────────────────────────────────────────
   The flyout's second zone, present only for an authenticated session
   (lib/presenter-tools.js). A prospect never has this in their DOM. */

/* In-session section hiding. A data attribute rather than an inline style,
   deliberately: sections-data.js writes style.display from the saved config on
   every SiteConfig update, and if the presenter's change used the same property
   the next update would silently undo it mid-meeting. */
[data-presenter-hidden] { display: none !important; }

.ptools {
  margin-top: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.ptools__head { display: flex; flex-direction: column; gap: 2px; }

.ptools__eyebrow {
  font-size: 10px;
  font-weight: var(--weight-bold);
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--accent);
}

.ptools__note { font-size: 12px; color: var(--ink-mute); }

.ptools__group { display: flex; flex-direction: column; gap: 6px; }

.ptools__label {
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 2px;
}

.ptools__check {
  display: flex;
  align-items: center;
  gap: 0.6em;
  font-size: 14px;
  color: var(--ink);
  cursor: pointer;
}
.ptools__check input { accent-color: var(--accent); cursor: pointer; }

.ptools__reset,
.ptools__link {
  align-self: flex-start;
  padding: 8px 16px;
  border: 1px solid var(--rule);
  background: transparent;
  font-family: inherit;
  font-size: 10.5px;
  font-weight: var(--weight-medium);
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-mute);
  text-decoration: none;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.ptools__reset:hover,
.ptools__link:hover { color: var(--accent); border-color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .ptools__reset, .ptools__link { transition: none; }
}

/* ── The pair toggle ──────────────────────────────────────────────
   Capabilities OR Areas of Expertise; Perspectives OR Models. One at a time,
   never both (Matt, 2026-07-27). Behaviour in lib/pair-toggle.js.

   The subtitle beneath the Perspectives/Models pills is culture language, so it
   is given real presence rather than being treated as a caption. */

.pair__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-xl);
}

.pair__pill {
  padding: 12px 26px;
  border: 1px solid color-mix(in srgb, var(--em) 45%, transparent);
  border-radius: var(--radius);
  background: transparent;
  color: var(--fg-mute);
  font-family: inherit;
  font-size: 12px;
  font-weight: var(--weight-medium);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.pair__pill:hover { border-color: var(--em); color: var(--em); }

.pair__pill.is-on {
  background: color-mix(in srgb, var(--em) 15%, transparent);
  border-color: var(--em);
  color: var(--em);
}

.pair__subtitle {
  margin-top: var(--space-md);
  max-width: 46ch;
  font-size: 16px;
  line-height: 1.5;
  color: var(--fg-soft);
}
.pair__subtitle:empty { display: none; }

/* Panels are hidden, never removed — animation state lives inside them. */
[data-pair-panel][hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .pair__pill { transition: none; }
}

/* ── Screen 4 · testimonials, above the logo marquee ───────────────
   The motion hierarchy lives here as much as in the JS. Quotes get scale,
   contrast and the text measure; the marquee below is small and low-contrast.
   Two loops at the same visual weight would feel cheap — different weight means
   the eye assigns a hierarchy instead of refereeing a fight. */

.quote-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: clamp(24px, 4vw, 56px);
  margin-top: var(--space-lg);
}

.quote-slot {
  /* The crossfade. Duration matches FADE_MS in lib/testimonials.js; if you
     change one, change the other. */
  transition: opacity 900ms var(--ease);
  opacity: 1;
  min-width: 0;
}
.quote-slot.is-fading { opacity: 0; }

.quote {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  border-top: 1px solid var(--rule-c);
  padding-top: var(--space-md);
}

.quote__text {
  font-size: clamp(18px, 1.9vw, 23px);
  font-weight: var(--weight-light);
  line-height: 1.42;
  letter-spacing: -0.012em;
  color: var(--fg);
  text-wrap: pretty;
}

.quote__attr {
  font-size: 12px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--em);
}

/* The marquee is the quieter register of the two: smaller, softer, and pushed
   down away from the quotes so the pair reads as foreground and background.

   30 JUL · The margin-top that used to live here is GONE, not moved. It was a
   SECOND .brand-stream declaration, ~850 lines after the first, and it silently
   won — so clearing the margin at the real rule did nothing and the new white
   band came out bottom-heavy by exactly this 56px. The spacing now belongs to
   .brand-band, which is the element that actually owns the gap.

   This is the third time this project has been bitten by one selector declared
   twice in one stylesheet with the later copy quietly winning (see the header of
   styles/motion.css, and the eyebrow rules in branch.css). Worth a sweep. */
.brand__name  { font-size: clamp(13px, 1.15vw, 15px); color: var(--fg-mute); text-align: center; }

@media (prefers-reduced-motion: reduce) {
  /* No crossfade at all; lib/testimonials.js also stops rotating. */
  .quote-slot { transition: none; }
  .quote-slot.is-fading { opacity: 1; }
}
