/* ============================================================
   components.css — the INTERNAL layout of every kit block render
   part in parts/blocks/. Enqueued after global.css and before
   blocks.css (inc/enqueue.php pins that order via dependencies).
   ------------------------------------------------------------
   MOBILE-FIRST, per DECISIONS D3 and its D3 UPDATE. Base rules are
   the 390px artboard; every query is min-width at 480 / 768 / 1024
   / 1200. There is no max-width query in this file and none may be
   added — the starter kit shipped this layer desktop-first and
   inverting it was the largest single piece of the build.

   Division of labor, so nothing is defined twice:
     global.css    reset, type scale, .wrap, .section, buttons,
                   header / nav / footer / modal / toast, .eyebrow,
                   .prose, [data-reveal]
     THIS file     per-block grids, cards, media frames, rails
     blocks.css    shared section chrome + .dark-bg legibility
     <block>.css   proof-bar, program-cards, network-map, roadshow,
                   team-cards, forms — already mobile-first and
                   conditionally enqueued; deliberately NOT
                   duplicated here

   Restyle targets: design-system.md §6 (per-block rows), §3.3 (the
   1px-gap hairline grid), §3.4 (measurements), §8.4 (stacking).
   Where §8 and D3 UPDATE disagree, D3 UPDATE wins: it is the
   authoritative responsive spec, and §8 is self-flagged as authored
   rather than extracted.

   Explicit px steps, never clamp(): §8.3 requires the desktop value
   to stay byte-identical to the design, which a viewport-
   interpolated value cannot guarantee at any particular width.

   Accent TEXT is var(--m434-accent-text) (#a34f0e, 5.70:1), never
   var(--red) (#E5761E, 3.03:1 on white). VSMA is DOE-funded, so
   Section 508 / WCAG 2.1 AA is contractual — see tokens.css F-16.
   The one exception is display type at 40px+, where tokens.css
   sanctions --red.

   D3's hide/keep list is honored by the per-block files (grid
   overlays, coord labels, corner ticks, pin pulses). What this file
   KEEPS at every size, because it carries the industrial language:
   inset 0 0 0 1px hairlines, the 3px accent bar, 1px grid gaps.
   ============================================================ */

/* ---------- Shared lede / intro lockup ----------
   15px/1.65 is the mobile design's body-lede size; 46ch keeps the
   measure honest without a magic pixel width (roadshow.css uses the
   same unit for the same role). */
.lede {
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.65;
  color: var(--m434-slate-strong);
  max-width: 46ch;
  margin: 14px 0 0;
}

/* ...and white at 72% on any dark ground. design-system.md §2.3 gives
   this exact value three times ("lede on dark", home hero / block hero
   / CTA), so it is the design's own token for the role rather than a
   local patch.

   The reported defect was the CTA banner: `cta ... has-bg-navy
   dark-bg` inherited the default --m434-slate-strong, i.e. #4b5563 on
   #262626 = 2.00:1, effectively invisible. .hero--bg had a private
   override (below); nothing else did. Scoping to .dark-bg fixes the
   CTA and every other band that grows a lede, and leaves the LIGHT cta
   variant — no bg_color, no photo, therefore no .dark-bg — untouched.

     rgba(255,255,255,.72) over --navy  #262626 -> #c2c2c2, 8.50:1
     rgba(255,255,255,.72) over crimson #8b2433 -> #dfc2c6, 5.28:1
       (has-bg-accent is --m434-crimson, not the orange, so it is a
        genuinely dark ground too)

   The photo variant needs its own selector because bg_color is
   optional there: an editor can set a background image and leave the
   band's color at "none", so .dark-bg is absent while the copy still
   sits on .cta:has(> .bg)::after — a 0.82-0.90 alpha #111 scrim. Worst
   case (a blown-out white photo under the lighter end of the scrim)
   the ground computes to #282828 and the lede to #c3c3c3 = 8.37:1. */
.dark-bg .lede,
.cta:has(> .bg) .lede { color: rgba(255, 255, 255, 0.72); }

/* ============================================================
   Hero — parts/blocks/hero.php
   ONE layout, TWO media treatments. Both are in the design:
     .hero            framed instrument panel beside the copy
                      (block library L:44-70 · §6 row 1 · grid
                      1.05fr .95fr gap 56px). Interior pages.
     .hero + .hero--bg  full-bleed photograph under a gradient scrim
                      on a fixed-height dark band (home page H:49-65
                      · §5.5 scrim · §5.1 grid · §5.6 hairline).
   The PHP picks between them from the `media_style` sub-field; the
   rationale for one layout rather than two lives in that file's
   docblock. Everything from .hero-copy inward is shared.
   ============================================================ */

/* One column at phone width. Source order is copy → media, which is
   also the order §8.4 asks for ("media below copy"), so no `order`
   is needed — and the h1 stays the first painted thing, which is
   what the LCP wants. The kit pulled the image above the copy with
   order:-1; that is reverted deliberately. */
.hero > .wrap {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
}

/* Design rhythm, mobile artboard: eyebrow mb 16 → lede mt 16 →
   buttons mt 28. Stepped up at md/lg to 18/18/30 and 22/22/34. */
.hero-copy .eyebrow { margin-bottom: 16px; }

/* h1 sizing is global.css's (32 → 40 → 52). Only the rhythm is ours. */
.hero-copy .lede { margin-top: 16px; }
.hero-copy .check-list { margin-top: 20px; }

/* Buttons live in their own row so the hero can carry two of them.
   Stacked and full-width below sm — the mobile artboard's decision,
   and two letter-spaced 26px-padded buttons do not fit at 390px. */
.hero-actions {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 28px;
}
.hero-actions .btn { width: 100%; }

/* --m434-muted-ink, not --slate. Every hero in this build is the
   .hero--bg photographic treatment, i.e. .dark-bg, and #6b7280 on the
   --navy under the scrim is 3.13:1 at 13px where AA wants 4.5:1. It
   renders that way today on the /blocks/ page. The sibling .lede three
   rules up already had a dark-ground value; this line was left behind.
   The token is --m434-slate-strong on light (7.56:1 on #fff, 6.93:1 on
   --bg-soft) and rgba(255,255,255,.8) on dark (10.17:1 on --navy,
   6.14:1 on crimson). */
.hero-note {
  font-family: var(--body);
  font-size: 13px;
  line-height: 1.6;
  color: var(--m434-muted-ink);
  margin: 16px 0 0;
}

/* The design frames hero media as an instrument panel, not a card:
   square corners, an accent hairline ring, a thin light mat. The kit
   had border-radius:18px + --shadow-lg (which is the MODAL shadow);
   both are wrong for this brand — the design has no border-radius
   anywhere. Natural image height rather than the design's fixed
   340px, so an editor's upload is never letterboxed. */
.hero-img {
  position: relative;
  background: var(--bg-soft);
  box-shadow: var(--m434-rule-accent);
  padding: 8px;
}
.hero-img img,
.hero-img video {
  display: block;
  width: 100%;
  height: auto;
}

/* ---------- .hero--bg: the full-bleed photographic band ----------
   min-height, never height. The mobile spec states the rule in its
   own words — "Decision: min-height 520px (never fixed)" — and it
   holds at every tier for the same reason: a longer heading, a
   larger default type size or a second CTA row must push the band
   open rather than be clipped by `overflow:hidden`. At the design's
   own copy length every tier renders at exactly the design's height.

   bg_color / spacing are NOT applied to this variant (see the PHP):
   the band paints and sizes itself, so a "Soft" ground would sit
   invisibly under an opaque dark one. blocks.css's default section
   rhythm is out-specificity'd by the padding below, which is what we
   want — this box is fully described here. */
.hero--bg {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  min-height: 520px;
  padding: 56px 0 44px;
  background: var(--navy);
}

/* Undoing the two-column grid with display:block also neutralises the
   grid-template-columns / gap / align-items that the lg block sets on
   `.hero > .wrap` — one rule instead of a mirror override per tier. */
.hero--bg > .wrap {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
}

/* Layer order in a photo band (§5.5): <img> → scrim → grid → content.
   Photo treatment steps toward the desktop values as the crop widens:
   40%/45% .42 → 34%/42% .46 → 28%/40% .5 (H:50). */
.hero-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 40% 45%;
  opacity: 0.42;
  filter: grayscale(0.3) contrast(1.05);
}

/* Vertical scrim on phones: the copy sits in the middle of the band
   rather than against its left edge, so a 90deg wash would leave the
   heading over the bright part of the photo. The horizontal version
   returns at md, where the copy is left-aligned in a wider band. */
.hero-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(17, 17, 17, 0.9) 0%,
    rgba(17, 17, 17, 0.72) 55%,
    rgba(17, 17, 17, 0.86) 100%
  );
}

/* §5.1's animated blueprint grid. Hidden below md per D3's hide list
   — 80px lines behind 32px display type is noise at phone width. */
.hero-grid { display: none; }

/* §5.6: the accent hairline the hero hands off to the proof band on.
   Kept as a pseudo-element so it needs no markup and cannot be
   scrolled away from the band's true bottom edge. */
.hero--bg::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(var(--vaccent, 229, 118, 30), 0.6),
    transparent
  );
}

/* The home hero's eyebrow is WHITE, not accent — the one place in the
   design where it is. .dark-bg .eyebrow (global.css) paints it
   accent-light, so this must out-specify it, which .hero--bg .eyebrow
   does at equal weight by loading later. */
.hero--bg .eyebrow {
  color: var(--white);
  letter-spacing: 0.18em;
}

/* .lede is dark slate by default; on the band it is white at 72%. Its
   46ch measure is left alone here — the phone artboard is narrower
   than 46ch anyway, so it only bites in the 480-767 band, which has
   no artboard and is better served by a capped line than by a 700px
   one. The design's own 520 / 560 caps take over at md and lg. */
.hero--bg .lede { color: rgba(255, 255, 255, 0.72); }

/* Entrance, H:55-58 — eyebrow .05s, h1 .18s, lede .32s, buttons .46s,
   all `.8s both`. Guarded by no-preference rather than left to
   global.css's reduced-motion block: that block clamps
   animation-duration but NOT animation-delay, so a `both` fill would
   hide the h1 for 180ms and the buttons for 460ms for precisely the
   users who asked for less motion. design-system.md §4 requires the
   entrance to be dropped outright under reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .hero--bg .eyebrow,
  .hero--bg h1,
  .hero--bg .lede,
  .hero--bg .hero-actions {
    animation: m434-hero-up 0.8s var(--m434-ease) both;
  }
  .hero--bg .eyebrow { animation-delay: 0.05s; }
  .hero--bg h1 { animation-delay: 0.18s; }
  .hero--bg .lede { animation-delay: 0.32s; }
  .hero--bg .hero-actions { animation-delay: 0.46s; }
}

@keyframes m434-hero-up {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: none; }
}

@keyframes m434-hero-grid {
  from { background-position: 0 0; }
  to   { background-position: 80px 80px; }
}

/* ============================================================
   Text + image — parts/blocks/text-image.php
   Design: §6 row 5 · grid .95fr 1.05fr gap 56px.
   ============================================================ */

/* .ti is on the .wrap element itself, so the container width and
   gutter are already global.css's job. The kit re-declared --maxw,
   margin:auto and a hardcoded 24px padding here via a
   `div:has(> .ti-img)` selector — a duplicate of .wrap that also
   overrode the token gutter. Both removed. */
.ti {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  align-items: center;
}

.ti-text .btn { margin-top: 22px; }

/* Same instrument-panel frame as the hero (§6 row 5: accent ring). */
.ti-img {
  position: relative;
  background: var(--bg-soft);
  box-shadow: var(--m434-rule-accent);
  padding: 8px;
}
.ti-img img,
.ti-img video {
  display: block;
  width: 100%;
  height: auto;
}

/* ============================================================
   CTA banner photo ground — parts/blocks/cta-banner.php
   ------------------------------------------------------------
   STOPGAP, and flagged as one. The part emits <img class="bg"> as
   the band's background plate, but no layer in the theme styles
   either class — so on the seeded home page that photo renders IN
   FLOW today: a full-width image shoved above the heading, inside
   the navy band. Lifting it out of flow is the smallest correct
   fix and it lives entirely in this file.

   This is not the cta_banner restyle §6 row 15 calls for (the 60px
   grid overlay, the 1000px container, the home split variant) — see
   followups. Specificity is kept to one or two classes so a later
   per-block stylesheet overrides it without !important.
   ============================================================ */

.cta {
  position: relative;
  overflow: hidden;
}

/* ---------- Photo band, generic ----------
   A `.bg` child is a full-bleed backdrop, never flow content. Without
   this the raw image renders at intrinsic size ABOVE the copy — a
   1254x836 photograph pushed the home testimonial's quote off screen.

   The rule was previously scoped to `.cta` alone, so it only covered
   the one block that shipped with a photo. `testimonial` gained an
   `image` field and inherited the bug. Scoping to `.section` covers
   every band that grows one later.

   design-system.md §6 row 9: the testimonial's crop is 60% 45% at
   .32 opacity, lighter than the hero's, because the quote sits over
   the center of the frame rather than the left third. */
.section:has(> .bg) {
  position: relative;
  overflow: hidden;
}

.section > .bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 60% 45%;
  opacity: 0.32;
  pointer-events: none;
}

/* Content has to be lifted above the backdrop explicitly — the image is
   a positioned sibling, so source order alone does not win. */
.section:has(> .bg) > .wrap,
.section:has(> .bg) > .quote,
.section:has(> .bg) > figure {
  position: relative;
  z-index: 2;
}

/* §5.5's hero photo treatment: half-opacity, lightly desaturated,
   framed off-center so faces survive the crop. The .cta variant keeps
   its own crop and opacity, overriding the generic rule above. */
.cta > .bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 28% 40%;
  opacity: 0.5;
  filter: grayscale(0.3) contrast(1.05);
}

/* Layer order in a photo band is img → scrim → content (§5.5). The
   design's scrim is a 90deg ramp that fades to .3 on the right,
   which works because desktop copy occupies only the left column.
   At phone width the copy spans the full band, so the base scrim is
   flat and the ramp returns at md with the split. */
.cta:has(> .bg)::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(17, 17, 17, 0.9) 0%, rgba(17, 17, 17, 0.82) 100%);
}
.cta > .wrap {
  position: relative;
  z-index: 2;
}

/* ============================================================
   CTA banner — the two variants (§6 row 15)
   ------------------------------------------------------------
     .m434-ctab--split   H:306-319. 1200px container, copy left /
                         a stacked button column right, left aligned,
                         h2 36px/1.02, eyebrow mb 16px.
     .m434-ctab--centre  L:316-323. 1000px container, centered, a single
                         button below the heading, h2 40px/1.05,
                         eyebrow mb 18px, button padding 0 40px.

   THE SPLIT COLLAPSES BY FLEX-WRAP, NOT BY A MEDIA QUERY — that is the
   artboard's own mechanism and it is why "the buttons move to the
   right" needs no breakpoint. H:308 sets flex-wrap:wrap and H:309 gives
   the copy `flex:1 1 480px`, so flex line-breaking (which uses the
   hypothetical main size) breaks the row as soon as 480 + the 40px gap
   + the actions column's intrinsic width exceeds the content box —
   around 810px of viewport at the 44px gutter with H:316's label. Below
   that the copy is alone on line 1 and grows to full width, the actions
   column drops to line 2, and `gap:40px` becomes the row-gap that the
   band has been missing entirely (measured 0px between the lede and the
   button). It also degrades correctly for any heading or label length,
   which a hardcoded breakpoint would not.
   ============================================================ */

/* H:307 and L:317, character for character in both files — so the
   overlay is NOT a library-only trait and ships on the home split band
   too. Two 1px repeating linear gradients on a 60px cell, never an
   image, and the ink composes from --m434-slate-rgb so every hairline
   alpha in the theme still comes from one source (.hero-grid uses the
   same triplet at .14).

   A real sibling div rather than a pseudo-element: `::after` on .cta is
   already the photo scrim's (.cta:has(> .bg)::after) and .m434-divider-*
   owns both, so a pseudo here would collide with whichever landed
   second. pointer-events:none matches .hero-grid and .m434-proof-grid.

   Not gated at 768: the mobile artboard's hide-list names the hero grid
   and the "proof-bar + testimonial grid overlays" and stops there
   (Mobile:585). This band is not on that list and is not drawn in the
   mobile file at all, so inventing a gate for it would be a guess. */
.m434-ctab-grid {
  position: absolute;
  inset: 0;
  /* ABOVE the photo scrim, which is z-index 1 (`.cta:has(> .bg)::after`).
     A tie would lose: on equal z-index a pseudo-element paints over a
     preceding sibling, so at 1 the grid was swallowed whole by the scrim
     on any band carrying a photo. `.wrap` is z-index 2 as well but comes
     LATER in source order, so the copy still sits on top of both. */
  z-index: 2;
  pointer-events: none;
  background-image:
    linear-gradient(rgba(var(--m434-slate-rgb), 0.12) 1px, transparent 1px),
    linear-gradient(90deg, rgba(var(--m434-slate-rgb), 0.12) 1px, transparent 1px);
  background-size: 60px 60px;
}

/* GRID + PHOTO: BOTH DRAW, and the `show_grid` toggle is what decides.

   This used to be `display: none` — the grid was suppressed outright on
   any band with a background image, on the reasoning that neither
   artboard draws the combination and that over a photograph the grid is
   noise the scrim exists to remove. That reasoning made the editor lie:
   `show_grid` is on by default, so switching a background image on
   silently turned a control that still read "on" into a no-op, with
   nothing in the interface to say why. A toggle that does nothing is a
   worse defect than a busy band, and the busy band is one click away
   from being fixed by the person looking at it.

   Nothing else is needed to make the pair legible: the scrim is a
   0.90 -> 0.82 alpha #111 ramp, so the composited ground under the grid
   is within a couple of points of the plain dark band the hairlines were
   measured against, and 0.12 slate reads the same over both. */

/* ---------- Split ---------- */
.m434-ctab--split .m434-ctab-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 40px;                       /* H:308 — and the row-gap once wrapped */
}

/* min-width:0 releases the flex item's `auto` minimum so a long
   uppercase word cannot push the copy column past the viewport; the
   480px basis is a preference, not a floor. */
.m434-ctab--split .m434-ctab-copy {
  flex: 1 1 480px;                 /* H:309 */
  min-width: 0;
}

/* ---------- Centered ---------- */
/* A column with a 30px gap rather than a margin on the h2: L:322's
   30px is heading-to-BUTTON, and that variant has no lede at all
   (L:318-322 is eyebrow -> h2 -> button), so driving it from the gap
   keeps the spacing right whether or not an editor fills the body. */
.m434-ctab--centre .m434-ctab-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 30px;                       /* L:322 */
  max-width: 1000px;               /* L:318 — no token; this is the only
                                      1000px container in the design */
  text-align: center;
}
.m434-ctab--centre .m434-ctab-copy { width: 100%; }
.m434-ctab--centre .m434-ctab-copy .lede { margin-left: auto; margin-right: auto; }

/* ---------- Actions column ----------
   H:314: a column, 14px apart. Full-width buttons below sm — Mobile:147
   states the decision ("CTAs stack full-width") and Mobile:162-163 draws
   it as 52px full-width children of a column flex. Released at 480,
   which is where .btn-block and .m434-btn-row already release for the
   same reason, so this band steps with the rest of the theme instead of
   inventing a breakpoint.

   Between 480 and the flex-wrap break the buttons are auto-width and
   still stacked at the left of their own line. That is what the
   artboard's own flex-wrap produces, not a defect.

   HEIGHT IS DELIBERATELY NOT FORKED. Mobile:576 gives a 52px mobile
   floor and H:315 draws 56px; .btn-lg is already 56px and cites H:315,
   and 56 clears a 52px floor, so a one-off mobile height here would
   desynchronise this band's controls from every other .btn-lg for 4px. */
.m434-ctab-actions {
  display: flex;
  flex-direction: column;
  gap: 14px;                       /* H:314 */
  width: 100%;
}
.m434-ctab-actions .btn { width: 100%; }
.m434-ctab--centre .m434-ctab-actions { align-items: center; }

/* ---------- Copy rhythm ----------
   H:310/H:311/H:312 · L:319/L:322. `h2:last-child` zeroes the heading's
   bottom margin when no lede follows, so the center variant's 30px gap
   is not doubled. */
.m434-ctab--split .m434-ctab-copy .m434-eyebrow { margin-bottom: 16px; }   /* H:310 */
.m434-ctab--centre .m434-ctab-copy .m434-eyebrow { margin-bottom: 18px; }  /* L:319 */

.m434-ctab-copy h2 { margin: 0 0 14px; }             /* H:311 */
.m434-ctab-copy h2:last-child { margin-bottom: 0; }
.m434-ctab-copy .lede { margin-top: 0; }             /* H:312 — the h2 owns the gap */

@media (min-width: 480px) {
  .m434-ctab-actions { width: auto; }
  .m434-ctab-actions .btn { width: auto; }
}

/* Both h2 rules live in ONE lg block on purpose: `h2 { font-size:44px }`
   sits inside @media (min-width:1200px) in global.css and comes later in
   the cascade, so an equally-specific selector would lose there. At
   (0,2,1) and (0,2,1) these win on specificity at every width from 1024
   up, regardless of order. Below 1024 the generic ladder stands — the
   mobile artboard never draws this band, so a CTA-specific mobile h2
   would be invented rather than cited.

   The 14px lede is fidelity, not accessibility (WCAG sets no minimum
   type size, and the pairing measures 7.75:1 over a grid line). There
   is direct precedent for the exemption: `.hero--bg .lede` already opts
   out of the same 16px lg step, immediately above. */
@media (min-width: 1024px) {
  .m434-ctab--split .m434-ctab-copy h2 { font-size: 36px; line-height: 1.02; }   /* H:311 */
  .m434-ctab--centre .m434-ctab-copy h2 { font-size: 40px; line-height: 1.05; }  /* L:322 */

  .m434-ctab-copy .lede { font-size: 14px; line-height: 1.6; max-width: 540px; } /* H:312 */

  /* L:321. The split band keeps .btn-lg's 0 32px, which is H:315's. */
  .m434-ctab--centre .m434-ctab-actions .btn { padding: 0 40px; }
}

/* ============================================================
   Checklist — parts/blocks/checklist.php
   Also the hero's bullet list, which shares .check-list markup.
   Design: §6 row 6 · 2-up hairline grid, 20px accent check box.
   ============================================================ */

/* Base treatment: a plain ruled-free list. This is the form the
   list takes inside .hero-copy, where a hairline table would fight
   the hero lockup. No `color` is set on the row — the hero can sit
   on a dark band, and an explicit --ink here would go invisible. */
.check-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
  max-width: var(--m434-measure-wide);
}
.check-list li {
  display: flex;
  align-items: center;
  gap: 14px;
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.5;
}

/* 20px box, accent hairline ring, 12px tick (§6 row 6). */
.check {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--red);
  box-shadow: var(--m434-rule-accent);
}
.check svg {
  width: 12px;
  height: 12px;
  display: block;
}

/* The checklist BLOCK gets the hairline-table treatment. Direct
   child of .wrap distinguishes it from the hero's copy of the same
   markup (which nests inside .hero-copy) with no markup change:
   checklist.php renders .wrap > ul.check-list. */
.wrap > .check-list {
  gap: 1px;
  background: var(--line);
  box-shadow: 0 0 0 1px var(--line);
}
.wrap > .check-list li {
  background: var(--white);
  color: var(--ink);
  /* 64px is D3's floor for a list row; align-items:center then does
     the vertical centering for one- and two-line rows alike. */
  min-height: 64px;
  padding: 18px 20px;
}

/* ============================================================
   Program body — the .m434-blocks breakout, TSK-04455 item 7.

   single-program.php emits `.m434-blocks.m434-program-body > .prose`.
   .prose keeps the typography; these rules move the WIDTH decision off
   .prose and onto its children, so an acf/* section can go full-bleed
   while a paragraph keeps exactly the measure it had before.

   The arithmetic matters. `.m434-blocks > *` (global.css) caps bare
   children at --m434-measure INCLUDING --m434-gutter of padding, which
   would have taken this page's text from 760px to ~672px — a page the
   client already said was too tight. The calc() below adds the gutter
   back, so the text column is unchanged and the only thing this commit
   moves is the blocks that were being clamped.
   ============================================================ */

.m434-program-body > .prose {
  max-width: none;
  padding-inline: 0;
}

.m434-program-body > .prose > :not(section):not(.alignfull):not(.alignwide) {
  max-width: calc(var(--m434-measure) + (var(--m434-gutter) * 2));
  margin-inline: auto;
  padding-inline: var(--m434-gutter);
}

/* Sections and explicit alignments break out. .alignwide stops at the
   site's own --maxw so it lines up with every other wide band. */
.m434-program-body > .prose > section,
.m434-program-body > .prose > .alignfull {
  max-width: none;
  padding-inline: 0;
}

.m434-program-body > .prose > .alignwide {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--m434-gutter);
}

/* ============================================================
   Process steps — parts/blocks/process-steps.php
   Design: §6 row 7 · 3-up hairline grid, each cell an accordion.
   ============================================================ */

.m434-steps-wrap {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding-left: var(--m434-gutter);
  padding-right: var(--m434-gutter);
}

/* Stacked head at phone width; the design's flex row returns at md. */
.m434-steps-head {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 28px;
}

/* 13px accent text — this was var(--red) in the kit, i.e. 3.03:1.
   Matches global.css's .eyebrow metrics so the two read as one
   system. */
.m434-steps-eyebrow {
  display: block;
  font-family: var(--body);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  /* --m434-accent-ink rather than a .dark-bg restatement: the token
     already splits light/dark AND carries the crimson exception
     (rgb(255,150,60) is 4.04:1 on #8b2433, under 4.5:1 at 10px/500). */
  color: var(--m434-accent-ink);
}

.m434-steps-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: 26px;
  line-height: 1.08;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--ink);
  margin: 10px 0 0;
}

.m434-steps-intro {
  font-family: var(--body);
  font-size: 14px;
  line-height: 1.6;
  color: var(--m434-muted-ink);
  margin: 0;
}

/* FLEX, not grid, and that is deliberate. The row has to be N-up at
   lg for whatever N the repeater holds — 4 on the home page, 3 in the
   block library — and a percentage basis with flex-grow gives that for
   free where a fixed `repeat(4,1fr)` would not. (The basis started as
   `0`, which never wraps; TSK-04455 item 15 capped it, and FB-026 made
   it count-aware. Both live in the lg tier, not here.) gap:1px over the
   container background is the §3.3 hairline trick, so the seams between
   cards ARE the rules and stacked rows draw their own with no extra
   markup — and unlike the grids in this file it is slab-proof at any
   count, because a wrapped flex row's cards GROW to cover their row
   instead of leaving an empty track for the container to paint. */
.wsteps {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--line);
  box-shadow: 0 0 0 1px var(--line);
}

/* position:relative is load-bearing — .wstep-ico is positioned against
   this box and hangs 13px outside it onto the seam. Nothing in the
   chain from .wsteps up to .m434-steps may set overflow:hidden or the
   chips get clipped in half. */
.wstep {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--white);
  padding: 22px 18px;
  transition: background 0.2s var(--m434-ease);
}
.wstep:hover { background: var(--m434-hover-light); }

/* ---------- Step connector chip (§5.7, H:120) ----------
   A 26px white disc with a hairline ring and a 13px accent chevron,
   centered ON the 1px seam between two cards — half in one card, half
   in the next — NOT inline beside the label.

   One column (this base tier) has no vertical seam to straddle, so the
   mobile design (_design/VSMA - Mobile + Tablet.dc.html, row 3:
   "connectors survive, rotated 90 deg to bottom-center") moves it to
   the horizontal seam and turns the chevron from "next, to the right"
   into "next, below". Its own line quotes the exact declarations
   reproduced here: bottom:-13px; left:50%;
   transform:translateX(-50%) rotate(90deg).

   Color: the chevron is an ICON, so --red is in role (rule 2). As a
   graphic it is decorative anyway, and #E5761E on the #ffffff chip is
   3.03:1, which clears 1.4.11's 3:1 for non-text contrast even if it
   were treated as meaningful. The chip keeps its own white fill on
   card hover, which is what puts it visually on top of the seam. */
.wstep-ico {
  position: absolute;
  z-index: 4;
  bottom: -13px;
  left: 50%;
  transform: translateX(-50%) rotate(90deg);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--red);
  pointer-events: none;
}

/* Without an explicit box the inline chevron SVG (no width/height
   attributes) expands to fill its container. */
.wstep-ico svg {
  width: 13px;
  height: 13px;
  display: block;
}

/* "Rendered on every card except the last" (§5.7). The last card has
   no next step, so its chip would point at the outer ring. */
.wstep:last-child .wstep-ico { display: none; }

/* ---------- The step numeral ----------
   design-system.md §2.2 "step numeral": 36px, line-height 1, display
   family, 700, accent (H:122); 34px at the 768 artboard and 30px at
   390. It is the design's large orange display type, not a tracked
   micro-label — the kit shipped it as 11px/0.16em/--slate, which is
   why the block read as a different component entirely.

   COLOR. At 30/34/36px 700 this is WCAG "large text", so the
   threshold is 3:1, not 4.5:1. That still rules --red out: #E5761E is
   3.03:1 on #ffffff (the 2.93 figure once stated in tokens.css was arithmetically wrong —
   see the report) and 2.77:1 on the #f4f5f7 the card hovers to, so it
   FAILS in a state the user can reach. --red-dark #c95f0f is 4.10:1 on
   #ffffff and 3.76:1 on #f4f5f7 — both clear 3:1 with room, and it is
   an existing sanctioned token rather than a new color. */
.wstep-label {
  display: block;
  font-family: var(--display);
  font-weight: 700;
  font-size: 30px;
  line-height: 1;
  letter-spacing: 0;
  text-transform: none;
  color: var(--red-dark);
}

/* Visually-hidden text. Belongs in global.css as a house utility —
   flagged in the report — but process-steps.php needs it now, so it is
   declared here and is safe to duplicate: the two rules would be
   identical. The numeral reads "01"; screen readers get "Step 1". */
.m434-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* 17px -> 19px -> 20px across the three artboards. margin:0 throughout
   — the card is a flex column and its `gap` owns the vertical rhythm,
   so any margin here would double-space one child.

   The .dark-bg guard is not cosmetic: global.css and blocks.css paint
   every heading inside a dark band white, and .wstep keeps a WHITE
   card fill on a dark band, so an unguarded h3 goes white-on-white.
   It needs THREE classes to win. blocks.css's
   `.dark-bg :is(h1,...,h6,.grad-text)` is not (0,1,1): :is() takes the
   specificity of its MOST specific argument, and .grad-text is a
   class, so the whole selector is (0,2,0) — a tie with
   `.dark-bg .wstep-title`, broken by blocks.css loading after this
   file. Verified in-browser: the two-class form lost. */
.wstep-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: 17px;
  line-height: 1.15;
  text-transform: uppercase;
  color: var(--ink);
  margin: 0;
}
.dark-bg .wstep .wstep-title { color: var(--ink); }

/* The optional `sub` field. design-system.md §6 row 7 maps it to the
   block library's step label — 11px/700/0.16em/uppercase (L:170) —
   whose color there is --slate. --slate is NOT used: #6b7280 is
   4.83:1 on #ffffff but 4.43:1 on the #f4f5f7 hover fill, i.e. it
   fails AA at 11px in the hover state. --m434-slate-strong is 7.56:1
   and 6.93:1 on those two grounds. */
.wstep-sub {
  font-family: var(--body);
  font-weight: 700;
  font-size: 11px;
  line-height: 1.45;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--m434-slate-strong);
  margin: 0;
}

.wstep-body-inner p {
  font-family: var(--body);
  font-size: 14px;
  line-height: 1.55;
  color: var(--m434-slate-strong);
  margin: 0;
}
.dark-bg .wstep-body-inner p { color: var(--m434-slate-strong); }

/* OPT-IN collapse, exactly the shape LESSONS #15 requires of the
   scroll reveal. This build's process_steps is static — no
   process-steps.js exists and the part emits no aria-expanded — so an
   unconditional `display:none` here would make four fifths of the
   block's copy permanently unreachable with no affordance to open it.
   The hidden state therefore applies only under html.m434-steps-ready,
   which a future accordion script must set as its first statement.
   Failure mode without the script: every step reads as open. */
html.m434-steps-ready .wstep[aria-expanded="false"] .wstep-body { display: none; }

/* ============================================================
   FAQ — parts/blocks/faq.php (native <details>)
   Design: §6 row 8 · hairline-separated rows, + rotating to ×.
   ============================================================ */

/* The design separates items with a single top rule rather than
   boxing each one; the kit boxed them in 12px-radius cards. */
.faq {
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  margin: 0;
  padding: 0;
  background: none;
}
.faq:last-of-type { border-bottom: 1px solid var(--line); }

.faq > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* D3's 64px list-row floor; the padding alone would give 58px. */
  min-height: 64px;
  padding: 18px 2px;
  cursor: pointer;
  list-style: none;
  font-family: var(--display);
  font-weight: 700;
  font-size: 17px;
  line-height: 1.25;
  /* inherit, not --ink: the block can carry a dark background, and
     .dark-bg sets the section color on the ancestor. */
  color: inherit;
}
.faq > summary::-webkit-details-marker { display: none; }

/* ---- The open/close toggle ----
   Drawn as an inline-SVG CSS MASK, not as `content: "+"`. This is the
   same technique and the same reasoning as .btn-arrow::after and
   .m434-arrowlink::before in global.css (LESSONS #19):

   GENERATED TEXT CONTENT IS IN THE ACCESSIBILITY TREE. With the "+"
   glyph in `content`, Chrome computed every question's accessible name
   as e.g. "Who is eligible for a VSMA assessment? +" — a stray token on
   the name of every <summary> in every FAQ block on the site. A
   pseudo-element painted through a mask contributes no text, so it is
   correctly absent; `aria-hidden` cannot be put on a pseudo-element,
   which is exactly why this technique is the answer rather than a
   nicety. Do NOT "simplify" it back to a glyph.

   Color: --m434-accent-ink, unchanged. What changes is which rule
   applies to it. As text it was 24px/400 — normal text by WCAG 2.1
   (the large-text allowance needs 24px BOLD or 18.66px+), so 1.4.3's
   4.5:1 governed. As a mask it is a graphical object and 1.4.11's 3:1
   does. The token clears both on every ground it can land on:
   #a34f0e on --white 5.70:1 · on --m434-hover-light 5.23:1;
   rgb(255,150,60) on --navy 6.98:1; #ffab5e on --m434-crimson 4.69:1.

   Geometry is now explicit (18px box) rather than font metrics, which
   also removes the dependency on the font actually carrying the glyph.
   The 45deg rotation that turns + into x still works: it rotates the
   masked box. */
.faq > summary::after {
  content: "";
  display: none;                /* turned on by the @supports below */
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  background-color: var(--m434-accent-ink);
  transition: transform 0.22s ease;
}

/* LONGHANDS, not the `mask: <url> no-repeat center / contain`
   shorthand — see global.css's note on the arrow: a UA with the
   prefixed longhand that chokes on the shorthand's `/ <size>` would
   pass the @supports gate, drop the declaration and paint a solid
   accent RECTANGLE. Longhands degrade one property at a time, and the
   pseudo-element is display:none outside this block, so "no mask
   support" degrades to "no icon". */
@supports ((-webkit-mask-image: none) or (mask-image: none)) {
  .faq > summary::after {
    display: block;
    -webkit-mask-image: var(--m434-plus-svg);
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    -webkit-mask-size: contain;
    mask-image: var(--m434-plus-svg);
    mask-repeat: no-repeat;
    mask-position: center;
    mask-size: contain;
  }
}

/* No mask support => no icon, so give the disclosure its native marker
   back rather than leaving the row with no affordance at all. (The
   arrows in global.css can degrade to nothing because the button label
   still carries the meaning; a <summary> has no such spare signal.) */
@supports not ((-webkit-mask-image: none) or (mask-image: none)) {
  .faq > summary { list-style: revert; }
  .faq > summary::-webkit-details-marker { display: revert; }
}

/* A masked pseudo cannot resolve to a correct forced system color, and
   in forced-colors the native marker is repainted for us anyway. */
@media (forced-colors: active) {
  .faq > summary::after { display: none; }
  .faq > summary { list-style: revert; }
}

.faq[open] > summary::after { transform: rotate(45deg); }
.faq > summary:hover { color: var(--m434-accent-ink); }

.faq .prose {
  max-width: 720px;
  padding: 0 2px 24px;
  font-size: 15px;
  line-height: 1.65;
}

/* ============================================================
   Stat band — parts/blocks/stat-band.php ("By the numbers")
   Design: §6 row 3 · 4-up hairline grid, band-matched cell fill.
   ============================================================ */

/* THE HAIRLINES ARE DRAWN BY THE CELLS, NOT BY THE CONTAINER. The
   canonical write-up of the technique is team-cards.css:19-39; this is
   the fourth and last grid to get it, and FB-037 is the report that
   pulled it in: "if the stat block doesn't have 4 items, it adds gray
   blocks (works properly on mobile)."

   A --line background on the CONTAINER showing through 1px gaps paints
   every EMPTY CELL as a solid gray slab, so any count that is not an
   exact multiple of the column count renders as broken rather than as
   short. Every comp was drawn with four stats, and the base tier is a
   single column — which is exactly why "works properly on mobile" and
   why nobody saw it until an editor entered three.

   8ff15df fixed the team, program and icon grids this way and left this
   one out ON PURPOSE: these cells go TRANSPARENT on a dark band, so the
   container background was doing two jobs at once — drawing the rules
   AND tinting the cells — and could not simply be deleted. The two band
   blocks further down now carry the second job: each wash moved onto the
   cell at the SAME alpha over the SAME ground, so every contrast ratio
   quoted below still describes the rendered pixels. Recomputed to
   confirm it: rgba(255,255,255,.16) over --navy #262626 is #494949
   resting and #525252 hovered, rgba(0,0,0,.22) over --m434-crimson
   #8b2433 is #6c1c28 and #752a35 — byte for byte the four values those
   notes already state.

   TWO consequences worth writing down, because neither is a mistake:

   1. --line is TRANSLUCENT (rgba(107,114,128,.35)), so the two rings
      that overlap inside a 1px gap composite to .5775 rather than .35:
      an interior seam is a hair darker than the container painted, while
      the outer edge is unchanged at .35. The three grids converted in
      8ff15df already render exactly that way, so this brings the fourth
      into line rather than introducing an oddity of its own.

   2. On a dark band the rules become VISIBLE for the first time. The
      container wash and the transparent cells were the same color, so
      the "rules go white" the note below has always claimed never
      actually drew — the panel was one flat rgba(255,255,255,.16)
      rectangle. Per-cell rings at the same alpha overlap to .2944 and
      the seams appear, which is the shape proof-bar.css draws with its
      rgba(255,255,255,.14) border-left. That is the design, but it IS a
      visible change to navy and crimson bands, not a no-op. */
.stats {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  text-align: center;
}

.stats > .stat {
  box-shadow: 0 0 0 1px var(--line);
}

.stat {
  background: var(--white);
  padding: 26px 20px;
  transition: background 0.2s var(--m434-ease), box-shadow 0.2s var(--m434-ease);
}

/* The design's tile hover: fill + a 3px accent cap (§6 row 3). The
   kit's translateY(-3px) is dropped — it shifts the hairline table
   out of alignment with its own outer ring.

   THE RING IS RESTATED HERE, AND IT HAS TO BE. box-shadow is not
   additive across rules, and `.stats > .stat` is (0,2,0) — the same as
   `.stat:hover`, so declaration order is all that separates them. A bare
   `inset 0 3px 0 var(--red)` here would REPLACE the ring and the cell
   would lose its hairlines under the pointer. This is the one thing that
   made the icon-card conversion non-mechanical: `.icon-card:hover`
   changes nothing but background, so its ring transferred untouched. */
.stat:hover {
  background: var(--m434-hover-tile);
  box-shadow: 0 0 0 1px var(--line), inset 0 3px 0 var(--red);
}

/* §3.3: this grid's cells are band-matched, not white, wherever the
   band itself is the soft fill. */
.has-bg-soft .stat { background: var(--bg-soft); }

/* …and the hover fill has to be restated for that band, which is a
   second defect this pass turned up rather than caused. `.has-bg-soft
   .stat` is (0,2,0), identical to `.stat:hover`, and it is declared
   LATER — so on a Soft band the tile has been swallowing its own hover
   fill and only the accent cap moved. The note below already measures
   --red-dark against "the --m434-hover-tile they fade to", i.e. the
   fade is the documented intent; (0,3,0) here is what makes it true. */
.has-bg-soft .stat:hover { background: var(--m434-hover-tile); }

/* On a dark band the cells take a translucent white wash and the rules
   go white — the same language proof-bar.css uses for the same shape.
   Wash and ring both sit on the CELL now (see the note at the top of
   this block); the alpha is the container's old one, so the resting
   composite over --navy is the same #494949 it always was. */
.dark-bg .stats > .stat {
  background: rgba(255, 255, 255, 0.16);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
}

/* The hover used to be a SECOND paint — rgba(255,255,255,.05) on the
   cell over the container's .16 — and one element cannot stack two
   background colors, so the two alphas are composited into one:
   1 - (1 - .16)(1 - .05) = .202, which over --navy is #525252, exactly
   what the two layers produced. Left as an rgba rather than that hex for
   two reasons: the wash still follows the band if --navy ever moves, and
   a color still TRANSITIONS (the other exact-composite option, a
   background-IMAGE layer over the wash, would snap instead of fading). */
.dark-bg .stats > .stat:hover {
  background: rgba(255, 255, 255, 0.202);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16), inset 0 3px 0 var(--red);
}

/* ...except on crimson, where a white wash is a contrast trap. What the
   numerals actually sit on is the wash, whichever element paints it.
   Over --navy that is #494949 and everything still works; over
   --m434-crimson #8b2433 a white wash lifts the ground to #9e4754, and
   #9e4754 is a mid-tone that nothing in this palette clears:

     rgba(255,255,255,.72) label   3.97:1   (3.66 on hover) — need 4.5
     --m434-accent-lt numeral      2.79:1   (2.51 on hover) — need 3
     #ffab5e numeral               3.24:1   (2.92 on hover) — need 3

   So on crimson the wash inverts. rgba(0,0,0,.22) takes the cell to
   #6c1c28 (#752a35 hovered) and every pairing clears with room —
   label 7.80:1 / 6.88:1, numeral 5.26:1 / 4.53:1. rich-text.css
   darkens its callout on dark bands for exactly this reason, so the
   two blocks now answer the same finding the same way. Declared after
   the `.dark-bg` pair above at equal specificity — (0,3,0) resting and
   (0,4,0) hovered on both — and resolved by order, which is why these
   two rules must stay BELOW them. */
.has-bg-accent .stats > .stat {
  background: rgba(0, 0, 0, 0.22);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.22);
}

/* Same compositing problem as the dark hover above, with the extra twist
   that the two layers are DIFFERENT colors — black .22, then white .06
   on top — so the single equivalent is not white. Solving
   a·R + (1 - a)·G for the pair: 1 - a = .94 × .78 = .7332, so
   a = .2668 and R = .06 × 255 / .2668 = 57.3. rgba(57,57,57,.267) over
   --m434-crimson lands on #752a35, the hovered hex the note above
   already quotes, so the three ratios it measures are untouched. */
.has-bg-accent .stats > .stat:hover {
  background: rgba(57, 57, 57, 0.267);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.22), inset 0 3px 0 var(--red);
}

/* 44 → 40 → 66. The tablet step DOWN is real: it is in the mobile
   design's "Values for CSS" sheet (D3 UPDATE), because 768px is
   where the tiles first sit in a row and 44px overflows the track.
   proof-bar.css runs the identical curve for the same reason.

   COLOR. tokens.css sanctions --red for display type at this size,
   and at 44-66px/700 the threshold really is 3:1 rather than 4.5:1 —
   but --red only clears it on WHITE. Measured: #E5761E is 3.03:1 on
   #ffffff, 2.77:1 on --bg-soft (which is exactly what
   `.has-bg-soft .stat` paints these cells) and 2.60:1 on the
   --m434-hover-tile they fade to. --red-dark #c95f0f is 4.10 / 3.75 /
   3.52 on the same three grounds — the identical substitution, for the
   identical reason, that .wstep-num two blocks up already makes.

   This used to carry two neutralizations — `background: none` and
   `-webkit-text-fill-color: currentColor` — because stat-band.php put
   .grad-text on this element and the gradient background-clip plus the
   transparent fill color swallowed any color set here. F-39 took the
   class off the markup (the PHP-side fix this comment asked for), so
   both are gone and the color above is simply the color. */
.stat-val {
  font-family: var(--display);
  font-weight: 700;
  font-size: 44px;
  line-height: 1;
  color: var(--red-dark);
  /* Tabular figures so a row of numbers aligns on its digits. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.dark-bg .stat-val { color: var(--m434-accent-lt); }

/* --slate was 4.43:1 on the --bg-soft these cells take on a Soft band,
   and the dark restatement rgba(255,255,255,.72) was 3.97:1 on the
   crimson composite below. One token covers both grounds. */
.stat-label {
  font-family: var(--body);
  font-size: 13px;
  line-height: 1.45;
  color: var(--m434-muted-ink);
  margin-top: 8px;
}

/* ============================================================
   Icon cards — parts/blocks/icon-cards.php
   Design: §6 row 4 · 3-up hairline grid, cell 34px 28px.
   ============================================================ */

.icon-cards-heading {
  text-align: center;
  max-width: 720px;
  margin: 0 auto 28px;
}

/* Hairlines drawn by each CARD's own ring, not by a background on the
   container showing through the gaps — see the note in team-cards.css.
   A container background paints every EMPTY CELL as a solid gray block,
   so any count that is not an exact multiple of the column count renders
   as broken rather than as short. */
.icon-cards-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
}

.icon-cards-grid > .icon-card {
  box-shadow: 0 0 0 1px var(--line);
}

/* The cell may render as <a> or <div> depending on whether the row
   has a link, so nothing here may assume either. */
.icon-card {
  display: flex;
  flex-direction: column;
  background: var(--white);
  padding: 26px 22px;
  transition: background 0.2s var(--m434-ease);
}
.icon-card:hover { background: var(--m434-hover-light); }

.icon-card-icon {
  display: block;
  width: 46px;
  height: 46px;
  flex: 0 0 auto;
}
.icon-card-icon img,
.icon-card-icon svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.icon-card-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: 19px;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--ink);
  margin: 18px 0 8px;
}

.icon-card-body {
  font-family: var(--body);
  font-size: 14px;
  line-height: 1.55;
  color: var(--m434-slate-strong);
  margin: 0;
}

/* margin-top:auto pins the cue to the cell floor so every card's cue
   aligns regardless of body length. 44px is D3's touch floor, taken
   as min-height so the text is not visually padded. */
.icon-card-cue {
  margin-top: auto;
  padding-top: 16px;
  min-height: 44px;
  display: inline-flex;
  align-items: flex-end;
  gap: 6px;
  font-family: var(--body);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--m434-accent-ink);
}
.icon-card-cue svg {
  width: 16px;
  height: 16px;
  flex: none;
}

/* ============================================================
   Logo strip — parts/blocks/logo-strip.php ("Trusted by")
   Design: §6 row 11 · repeat(4,1fr) gap 20px, 64px hairline tiles.
   ------------------------------------------------------------
   TWO DELIBERATE DEVIATIONS FROM THAT ROW, both from the client's
   2026-09-08 feedback and both approved by the lead:
     · the 4/3/2 row is a wrapping FLEX row rather than a 1fr grid, so
       a ragged last row centers instead of hanging left (FB-043c);
     · the white tile + hairline is painted only on the dark bands,
       where a logo needs a plate to stay visible (FB-043a).
   The tile GEOMETRY — 56 → 64px tall, 8px padding, centered — is §6
   row 11 unchanged. A third addition, the `slider` layout, lives with
   the shared rail further down rather than here (FB-044).
   ============================================================ */

/* A WRAPPING FLEX ROW, NOT A GRID — and the ragged last row is the
   reason. §6 row 11 draws `repeat(4,1fr) gap:20px`, which this
   reproduced literally, but `1fr` tracks consume the whole container:
   five logos put the fifth alone in column 1 and `justify-content` has
   no free space left to center it, so the strip reads as left-aligned
   with a hole on the right (FB-043c). A flex row with an explicit
   per-row basis keeps the SAME 2 / 3 / 4 counts the grid had — the
   basis is `(100% − that row's gaps) / n`, so a FULL row is
   pixel-identical to a 1fr track — and a short row centers.

   --m434-logo-basis is the only thing the md / lg steps below change,
   and the gap lives in its own custom property so the two cannot drift
   apart (n columns carry n−1 gaps). Two columns is the floor from
   §8.4; four 140px logos in a row do not survive 390px. */
.logos {
  --m434-logo-gap: 20px;
  --m434-logo-basis: calc((100% - var(--m434-logo-gap)) / 2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--m434-logo-gap);
  align-items: center;
  justify-content: center;
}

/* ONE TILE BOX FOR BOTH MARKUP SHAPES. logo-strip.php emits
   `<a><img></a>` for a linked logo and a bare `<img>` for an unlinked
   one, and this rule is what makes those two the same size. They were
   not: as a grid item the `<a>` stretched to its whole 1fr track,
   while the bare `<img>` — `width: auto` against a specified `height`
   — took only the width its aspect ratio asked for, so a linked logo
   occupied a visibly wider box than the unlinked one beside it
   (FB-043b, and exactly what the client reported). Both now take the
   same basis and the same padding, and each centers its own artwork:
   the `<a>` by being a centering flex box, the bare `<img>` by
   `object-fit` inside the box the basis gives it.

   min-width: 0 is load-bearing rather than defensive. A flex item's
   automatic minimum size is content-based, and for a replaced element
   that is its intrinsic width — which would refuse to shrink below a
   wide logo's natural size and burst the row at 390px. */
.logos > a,
.logos > img {
  flex: 0 1 var(--m434-logo-basis);
  min-width: 0;
  height: 56px;
  padding: 8px;
  /* Reaches the bare <img> only; inert on a non-replaced <a>. */
  object-fit: contain;
}
.logos > a {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Scoped to the LINKED case on purpose. The old `.logos img` also
   matched the bare `<img>` ROW ITEM, where `width: auto` is precisely
   the bug above — and at equal specificity to the box rule, declared
   later, it would win and un-size it again. Inside the link the image
   is a flex item of the tile's content box — 40px tall at base, 48px
   from md — so max-height + contain is what keeps a wide or a tall
   logo in proportion. */
.logos > a > img {
  width: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* THE WHITE PLATE IS DARK-GROUND ONLY NOW. §6 row 11 gives every tile
   `#fff` + `inset 0 0 0 1px rgba(107,114,128,.3)`; on the None / Soft
   / Cool bands that draws a box around each logo which the client read
   as chrome she had not asked for (FB-043a — a deviation from the
   approved design, signed off by the lead 2026-09-08).

   It is NOT deletable, though, which is why this is a split and not a
   removal: bg_color navy and accent map to `.dark-bg`, which paints
   --navy or --m434-crimson, and a dark or transparent logo with no
   plate under it simply disappears there. `height` and `padding` stay
   in the base box above because they set the row's alignment on every
   band; only the plate moves here. */
.dark-bg .logos > a,
.dark-bg .logos > img {
  background: var(--white);
  box-shadow: var(--m434-rule-inset);
}

/* Desaturate-until-hover only where a pointer exists. Applied
   unconditionally it would leave every logo permanently gray on
   touch, since the hover that restores color never fires there.

   THE 0.75 FADE FOLLOWS THE PLATE. grayscale + 0.75 opacity was tuned
   against the white tile: the logo washed toward #fff, which reads as
   quiet. With the plate gone on the light bands the same fade washes
   toward --bg-soft #f4f5f7 instead and the mark goes muddy rather than
   quiet, so the at-rest opacity holds at 1 wherever there is no plate
   and keeps its 0.75 wherever there still is one. Reducing a fade can
   only raise contrast, so this cannot cost legibility. The grayscale
   itself stays on every band — that is the treatment the design asks
   for — and hover restores full color in both cases. */
@media (hover: hover) {
  .logos img {
    filter: grayscale(1);
    transition: filter 0.2s ease, opacity 0.2s ease;
  }
  .dark-bg .logos img { opacity: 0.75; }
  .logos > a:hover img,
  .logos > img:hover {
    filter: none;
    opacity: 1;
  }
}

/* ============================================================
   Downloads — parts/blocks/downloads.php ("Resources")
   Design: §6 row 13 · ringed list, one hairline-separated <a>/row.
   ============================================================ */

.downloads {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--m434-measure-wide);
  background: var(--white);
  box-shadow: 0 0 0 1px var(--line);
}
.download { margin: 0; }

/* Rule between rows only — a border on every row would double up
   against the list's own outer ring. */
.download + .download .download-link { border-top: 1px solid var(--line); }

/* §8.4: the meta drops below the name at narrow widths, so the row
   is a column first and becomes the design's split row at md. */
.download-link {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 6px;
  min-height: 64px;
  padding: 18px 20px;
  text-decoration: none;
  transition: background 0.18s var(--m434-ease);
}
.download-link:hover { background: var(--m434-hover-light); }

.download-label {
  font-family: var(--body);
  font-weight: 500;
  font-size: 15px;
  line-height: 1.4;
  color: var(--ink);
}
/* --m434-muted-ink: --slate is 4.83:1 on the row's white ground but
   4.43:1 on the --m434-hover-light it fades to, i.e. an AA failure at
   12px in a state the pointer reaches. */
.download-meta {
  font-family: var(--body);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--m434-muted-ink);
}

/* ============================================================
   Posts feed + case-study cards — parts/blocks/posts-feed.php,
   parts/blocks/case-study-cards.php, parts/case-card.php
   Design: §6 row 14 · repeat(3,1fr) gap 24px, ringed link cards.
   ============================================================ */

/* F-40 CLEARED. Both containers used to carry an inline
   `repeat(auto-fit, minmax(280px,1fr))` grid, which beat every rule
   here — auto-fit collapses to one column on its own, so nothing
   looked broken at phone width, but the column count could not be
   tuned from a stylesheet at all. posts-feed.php no longer emits it,
   so these rules and their md/lg steps (1 -> 2 at 768 -> 3 at 1024,
   §6 row 14's repeat(3,1fr) gap:24px) are now the live grid. */
.posts-feed-grid,
.wrap:has(> .case-card) {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
}

.case-card,
.posts-feed-grid article,
.posts-feed-grid .post-card {
  display: flex;
  flex-direction: column;
  background: var(--white);
  box-shadow: var(--m434-rule-inset);
  transition: box-shadow 0.2s var(--m434-ease), transform 0.2s var(--m434-ease);
}
.case-card:hover,
.posts-feed-grid article:hover,
.posts-feed-grid .post-card:hover {
  box-shadow: inset 0 0 0 1px rgba(var(--vaccent, 229, 118, 30), 0.7);
  transform: translateY(-3px);
}

/* 170px thumb + a hairline foot, per §6 row 14. */
.case-media img,
.posts-feed-grid img {
  display: block;
  width: 100%;
  height: 170px;
  object-fit: cover;
  border-bottom: 1px solid var(--line);
}

.case-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 22px 22px 24px;
}

.case-tag {
  font-family: var(--body);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--m434-accent-text);
}

/* The card is always a white cell, even inside a dark band, so h3
   must not inherit .dark-bg's white. */
.case-card h3,
.dark-bg .case-card h3 {
  font-size: 17px;
  line-height: 1.25;
  color: var(--ink);
  margin: 0;
}

.case-arrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
  font-family: var(--body);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--m434-accent-text);
}
.case-arrow svg {
  width: 16px;
  height: 16px;
  flex: none;
}

/* ============================================================
   Team grid — parts/blocks/team-grid.php
   Mirrors team-cards.css so the two team blocks read as one system.
   ============================================================ */

.team-intro { margin-bottom: 28px; }

.team-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  background: var(--line);
  box-shadow: 0 0 0 1px var(--line);
}

.team-card {
  display: block;
  background: var(--white);
  padding: 26px 22px;
  transition: background 0.2s var(--m434-ease);
}
.team-card:hover { background: var(--m434-hover-light); }

/* A square accent-ringed plate, not a full-bleed banner: the design
   frames people the same way team-cards.css does. */
.tc-photo {
  width: 96px;
  height: 96px;
  margin-bottom: 14px;
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--m434-rule-accent);
}
.tc-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Faces sit above center in a portrait crop; plain centering cuts
     foreheads. */
  object-position: 50% 25%;
}

.tc-name {
  font-family: var(--body);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
}
/* Same hover-fill failure as .download-meta: --slate is 4.43:1 on the
   --m434-hover-light .team-card fades to. */
.tc-role {
  font-family: var(--body);
  font-size: 13px;
  line-height: 1.5;
  color: var(--m434-muted-ink);
  margin-top: 6px;
}

/* ============================================================
   Testimonial (single) — parts/blocks/testimonial.php
   ------------------------------------------------------------
   TWO VARIANTS, one part. §6 row 9 keys them off whether a photo
   is set, and testimonial.php emits the answer as a class:

     .m434-quote--photo   H:257-266. Ground #1f1f21, photograph at
                          .32 under a 90deg dual scrim with a crimson
                          tail, 60px blueprint grid at white .05, a
                          4px crimson left rail, LEFT aligned, 960px
                          measure, 30px/1.3 quote, crimson CTA.
     .m434-quote--band    L:206-215. Flat crimson, no photo, no scrim,
                          no rail, the same 60px grid at white .08,
                          CENTERED, 900px measure, 32px/1.28 quote.

   The rules below are ordered ground → layers → measure → type →
   byline → CTA. Everything shared stays on .quote / .cite so the
   testimonial_carousel card, which also carries the class `quote`,
   keeps inheriting it.
   ============================================================ */

/* .quote shares its element with .wrap, so this narrows the
   container global.css already sized (§3.1: 900px for this band). */
.quote {
  max-width: var(--m434-maxw-narrow);
  margin: 0 auto;
  text-align: center;
}

/* 22 → 26 → 30 → 32 (§8.3, read bottom-up). */
.quote blockquote {
  font-family: var(--display);
  font-weight: 700;
  font-size: 22px;
  line-height: 1.3;
  /* --m434-surface-ink, not a `.dark-bg .quote blockquote` pair: at
     (0,2,1) that rule reached inside .tc-item, a card that stays white
     on every band, and painted the pull-quote white on white (1.00:1,
     measured on a testimonial_carousel set to Navy). */
  color: var(--m434-surface-ink);
  margin: 0 0 24px;
}

/* F-50 — the pull-quote's “ ” marks, DRAWN rather than typed.
   testimonial.php and testimonial-carousel.php used to wrap the quote
   in two straight ASCII " characters in the template: not the marks
   the design draws (§6 row 10), not translatable — punctuation is
   language-specific and several locales do not use these glyphs at
   all — and decoration welded into content.

   `content: open-quote / close-quote` with the `quotes` property left
   at its initial `auto` resolves the pair from the element's language,
   so <html lang="en"> gets “ ” and a translated site gets its own
   marks with no CSS or template change. The <blockquote> element
   already carries the semantics; these are purely visual. */
.quote blockquote::before { content: open-quote; }
.quote blockquote::after  { content: close-quote; }
.tc-item blockquote::before { content: open-quote; }
.tc-item blockquote::after  { content: close-quote; }

/* Forced-colors keeps them: they are punctuation drawn in currentColor,
   not an icon that has to resolve to a system color. */

/* Byline stacks at phone width and becomes the design's row at sm.
   The kit relied on `margin:0 auto` on the avatar with no rule on
   the figcaption at all. */
.cite {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

/* .avatar and .co-logo are the <img> elements themselves. The kit
   targeted `.avatar img` / `.co-logo img`, which match nothing in
   the rendered markup — that is why the logo was unsized. */
.avatar {
  width: 46px;
  height: 46px;
  flex: 0 0 auto;
  border-radius: 50%;
  object-fit: cover;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}
.co-logo {
  max-height: 32px;
  width: auto;
  opacity: 0.8;
}

.name {
  font-family: var(--body);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--m434-surface-ink);
}

/* Both byline lines read surface tokens for the .tc-item reason above:
   `.dark-bg .name` / `.dark-bg .role` painted white and .7-white on a
   card that is still #ffffff — 1.00:1 each. */
.role {
  font-family: var(--body);
  font-size: 12px;
  line-height: 1.5;
  color: var(--m434-muted-ink);
}

/* ---------- The band itself ----------
   position + overflow are supplied to the PHOTO variant by
   `.section:has(> .bg)` above, but the crimson band has no photo, so
   that selector never matches it and its grid overlay would escape the
   section. Both variants declare it here instead.

   The grounds are (0,2,0) so they outrank the `.has-bg-*` fills at
   (0,1,0). testimonial.php no longer passes `bg_color` to
   m434_section_classes() — the variant owns the ground — but the
   specificity is kept because two instances still carry a stored
   `bg_color = navy` and a future template could re-emit it. */
.m434-quote-band {
  position: relative;
  overflow: hidden;
}

.section.m434-quote--photo { background: var(--m434-dark-3); }   /* H:257 */
.section.m434-quote--band  { background: var(--m434-crimson); }  /* L:206 */

/* H:258. opacity .32 and object-position 60% 45% already come from the
   generic `.section > .bg`; only the treatment is this band's. */
.m434-quote-band > .bg { filter: grayscale(.35) contrast(1.05); }

/* A PHOTO ON THE CRIMSON BAND IS SUPPRESSED, deliberately. L:206 has no
   photograph, and testimonial.php only emits the scrim on the photo
   variant — so an editor who forces variant=band while an image is
   stored would get an unscrimmed picture under the copy. Measured over
   a blown-out white frame at .32 on #8b2433 the ground composites to
   rgb(176,106,116): the 22-32px/700 blockquote holds at 4.07:1 (large
   text, 3:1) but the 13px attribution drops to 3.21:1 and FAILS 1.4.3.
   "Crimson band" means a flat band; the photo is what the other variant
   is for. */
.m434-quote--band > .bg { display: none; }

/* ---------- Overlay layers ----------
   §5.5's stack is img → scrim → grid → rail → content, all four
   position:absolute inset:0 with the content position:relative. They
   are REAL elements, not pseudo-elements, because ::after on this
   section is already .m434-divider-bottom's and ::before is
   .m434-divider-top's (global.css). `.section > .bg` is z-index 0 and
   the content is 2, so the layers sit at 1. */
.m434-quote-scrim,
.m434-quote-grid,
.m434-quote-rail {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

/* H:259 verbatim, restated in design-system.md §5.5 as .m434-scrim--quote.
   Note the CRIMSON tail at 100%: it is what ties the photo band to the
   crimson one, and it is also the lightest point of the ramp, so it is
   the stop every contrast figure below is measured at. */
.m434-quote-scrim {
  background: linear-gradient(
    90deg,
    rgba(20, 20, 22, 0.95) 0%,
    rgba(20, 20, 22, 0.82) 45%,
    rgba(139, 36, 51, 0.35) 100%
  );
}

/* The same three stops, raised, for a bright or busy photograph. There
   is deliberately no "off" and no "lighter": the scrim is the only
   thing holding 30px white type and a 13px attribution over an
   arbitrary editor upload, and an off-state that drops the quote to
   ~1.2:1 is LESSONS #17's trap rather than a preference. */
.m434-quote-scrim--strong {
  background: linear-gradient(
    90deg,
    rgba(20, 20, 22, 0.98) 0%,
    rgba(20, 20, 22, 0.9) 45%,
    rgba(139, 36, 51, 0.55) 100%
  );
}

/* ---------- Blueprint grid ----------
   Built the way the design builds it: two 1px repeating linear
   gradients on a 60px cell, NOT an image. Cell 60px 60px and the alpha
   are the artboards' own values — white .05 on the photo band (H:260),
   white .08 on the crimson one (L:207) — carried in a custom property
   so the two variants differ by one line instead of a second
   background-image list.

   The element also carries .m434-gridlines, which global.css already
   declares display:none at base and display:block inside
   @media (min-width: 768px). That is exactly the mobile artboard's
   "Hide below 768px … proof-bar + testimonial grid overlays", so the
   responsive behavior costs ZERO new media queries here. */
.m434-quote-grid {
  --m434-quote-grid-ink: rgba(255, 255, 255, 0.05);
  background-image:
    linear-gradient(var(--m434-quote-grid-ink) 1px, transparent 1px),
    linear-gradient(90deg, var(--m434-quote-grid-ink) 1px, transparent 1px);
  background-size: 60px 60px;
}
.m434-quote--band .m434-quote-grid { --m434-quote-grid-ink: rgba(255, 255, 255, 0.08); }

/* H:261 — photo variant only; testimonial.php's show_rail defaults to
   the variant, so the crimson band never emits this element. `inset:0`
   above sets all four sides, so the rail releases `right` and takes a
   width. */
.m434-quote-rail {
  right: auto;
  width: 4px;
  background: var(--m434-crimson);
}

/* ---------- Content ----------
   `.section:has(> .bg) > .quote` already lifts the photo variant's
   figure; the crimson band has no photo and needs its own. */
.m434-quote-band > .m434-quote-inner {
  position: relative;
  z-index: 2;
}

/* H:262. The band variant keeps .quote's 900px + centered (L:208).
   BOX MODEL, said out loud: the artboards are content-box, so H:262's
   `max-width:960px; padding:74px 44px` is a 960px TEXT MEASURE inside a
   1048px box. This theme is border-box and every other container in it
   — .wrap, .wrap-narrow, .wrap-quote — counts the gutter inside the
   max-width. Widening this one band to 1048px to chase the measure
   would desynchronise its left edge from every section above and below
   it, so --m434-maxw-quote is used as the CONTAINER, which is also how
   design-system.md §7 names it. The measure lands at 872px at lg. */
.m434-quote--photo .m434-quote-inner {
  max-width: var(--m434-maxw-quote);
  text-align: left;
}

/* H:263 / L:209 — the box-height trick. A 70px glyph on line-height .4
   computes a 28px line box inside a 30px-tall div, so the ink hangs
   above and below it while the blockquote below starts from that short
   box. NO overflow:hidden: the overflow IS the effect.

   Decorative and aria-hidden in the markup, which is what makes the
   contrast correct rather than a 1.4.3 failure — crimson on the
   scrimmed ground is 2.09:1 and accent on crimson 2.89:1. Do NOT
   "fix" either color; that changes the design and is not required. */
.m434-quote-mark {
  font-family: var(--display);
  font-weight: 700;
  font-size: 70px;
  line-height: 0.4;
  height: 30px;
  color: var(--m434-crimson);
}
.m434-quote--band .m434-quote-mark {
  height: 34px;
  color: var(--red);
}

/* CLEARANCE FOR AN EYEBROW ABOVE THE GLYPH. The box-height trick means
   the ink escapes its own box upward by roughly (70 − 28) / 2 = 21px, so
   an eyebrow immediately above it gets struck through. Neither artboard
   puts an eyebrow on this band (H:257-266 and L:206-215 both start at
   the glyph), but testimonial.php emits one whenever it is authored, and
   it is the first child when it is. Found by injecting one into the live
   band during verification: the glyph's ink measured as the EYEBROW's
   own background — rgb(229,118,30) on the crimson variant — which is
   also why the eyebrow's contrast could not be measured until this rule
   existed. The adjacent-sibling combinator costs exactly nothing on the
   two instances that ship today, neither of which has an eyebrow. */
.m434-eyebrow + .m434-quote-mark { margin-top: 22px; }

/* THE INLINE MARKS COME OFF THIS BAND — and only this band. Neither
   artboard wraps the single testimonial's text in “ ” (H:264, L:210);
   the leading glyph is the only mark and there is no closing one. Left
   in place they would draw two opening marks side by side.

   The scope matters: `.tc-item` ALSO carries the class `quote`
   (`<figure class="quote tc-item">`), so removing them from
   `.quote blockquote` would silently strip the CAROUSEL card's marks
   too — and §6 row 10 requires them there (L:234). .m434-quote-band is
   emitted only by testimonial.php, so the carousel is untouched.
   Same (0,1,1) specificity as the rule it overrides, declared after
   it, so source order decides. */
.m434-quote-band blockquote::before,
.m434-quote-band blockquote::after { content: none; }

/* Quote type, per variant. The shared 22 → 26 → 30 ladder on
   `.quote blockquote` stands; these are the leading and the rhythm the
   two artboards differ on (H:264 30px/1.3 margin 24/26 · L:210
   32px/1.28 margin 20/30), and the 1200 step to 32px now belongs to
   the band variant alone. */
.m434-quote--photo .m434-quote-inner blockquote { margin: 24px 0 26px; }
.m434-quote--band .m434-quote-inner blockquote {
  line-height: 1.28;
  margin: 20px 0 30px;
}

/* ---------- Byline ----------
   H:265 is ONE line, left aligned, and attributes to the role only
   ("Operations Director · Virginia Manufacturer") — no avatar and no
   name row. The part still emits the shared .cite markup, so the photo
   variant only has to unwind the centering .cite picks up at sm and give
   the role line the design's own type.

   COLOR IS A DELIBERATE DIVERGENCE. H:265 draws rgba(255,255,255,.6).
   Composited over the LIGHTEST point of the scrim — the crimson tail at
   100%, over a blown-out white frame at .32, ground rgb(116,80,85) —
   that is 3.67:1 at 13px, where 1.4.3 wants 4.5:1. The artboard is not
   exempt from Section 508. --m434-muted-ink is rgba(255,255,255,.8) on
   a dark ground and measures 5.15:1 on the same worst-case pixel (and
   8.60:1 over the real bg-0385.jpg), so the band takes the token the
   rest of the theme already uses for secondary text on dark. */
.m434-quote--photo .cite {
  align-items: flex-start;
  justify-content: flex-start;
  text-align: left;
}
.m434-quote--photo .role {
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ---------- CTA ----------
   H:266, "Read More Testimonials". The role is .btn-emphasis (solid
   crimson, white label, 8.76:1) on the photo band and .btn-secondary on
   the crimson one, both chosen in the part; no fifth button role.

   THE RING IS AN ACCESSIBILITY FIX, NOT A STYLE. As H:266 draws it the
   crimson fill sits on the scrimmed near-black band and the FILL-to-
   GROUND contrast is 2.09:1 at the dark end of the scrim — under
   1.4.11's 3:1 for the visual information that identifies a control, so
   the button's shape is nearly invisible even though its label is fine.
   --m434-btn-ring is rgba(255,255,255,.5) on every dark ground
   (global.css), which composites to rgb(197,146,153): 6.93:1 against
   the band and 3.31:1 against the crimson fill itself, so the boundary
   holds in both directions. Chrome layered on a role, the same shape as
   NOTE-ALERT-RING in global.css. */
.m434-quote-cta { margin-top: 30px; }
.m434-quote--photo .m434-quote-cta { box-shadow: inset 0 0 0 1px var(--m434-btn-ring); }

/* THE ATTRIBUTION MUST NOT SIT ON THE BUTTON. `.cite` carries no top
   margin of its own — nothing above it in the figure ever needed one,
   because `blockquote` has a bottom margin and the quote mark is
   absolutely positioned. The CTA has neither: it is an inline-flex
   `.btn` with `margin-bottom: 0`, so with both a button AND an
   attribution present the role line rendered flush against the
   button's bottom edge, measured at a 0px gap. That combination is
   reachable from the editor on either variant and looks like a
   collision rather than a layout.

   Scoped to the adjacency so the far commoner no-button case keeps
   the spacing the blockquote already gives it. 26px, not the CTA's
   own 30: this is a caption returning to its quote, not a control
   being set apart from one. */
.m434-quote-cta + .cite { margin-top: 26px; }

/* Both artboards draw the section hairline full bleed (H:257, L:206),
   but the shared divider mechanism insets it to the 1200px container by
   design (block-chrome.php). On a full-bleed colored band a rule that
   stops 120px short of each edge reads as a bug, so this one variant
   releases the cap. One editor control, one line — rather than a second
   hardcoded border that would draw twice when divider=bottom. */
.m434-quote-band.m434-divider-bottom::after,
.m434-quote-band.m434-divider-top::before { max-width: none; }


/* ============================================================
   The scroll-snap RAIL — two blocks, one implementation
   parts/blocks/testimonial-carousel.php
   parts/blocks/logo-strip.php (its `slider` layout)
   ------------------------------------------------------------
   THE CAROUSEL STAYS. design-system.md §6 row 10 and §10.5 decided
   this block should render a static 2-up grid with NO arrows, dots or
   track, and docs/TODO.md F-54 has tracked that PHP + enqueue change
   since 2026-08-19. The lead OVERRULED that on 2026-09-08: the client
   wants the carousel kept and polished (FB-042), and the logo strip
   turned into a slider like it (FB-044). So F-54 is "won't do — client
   wants the carousel", the rail is the shipping design rather than an
   interim state, and the machinery below is now SHARED instead of
   pending deletion. §10.5 still says the opposite and needs the
   reversal recorded.

   Declared AFTER .quote on purpose — .tc-item carries both classes
   and must win the tie on order.
   ============================================================ */

/* ---------- The rail mechanics: one rule set, two blocks ----------
   A selector list rather than a shared class, deliberately. `.tc-item`
   is pinned by blocks.css's light-island list — a file this change does
   not own — so the `tc-` family cannot be renamed to something
   block-neutral; and hanging `tc-track` on a `<div class="logos">`
   would be a lie in the markup plus a gap fight in the cascade
   (`.tc-track`'s 16px against `.logos`'s 20px, at equal specificity,
   decided by source order). Two selectors on one rule set cost nothing
   and keep both blocks honest about what they are. Read `tc-` as
   "track", not as "testimonial", everywhere below.

   The scrollbar is deliberately LEFT ON here and taken off only where
   the arrows replace it — see the lg + hover block. */
.tc-track,
.logos--slider {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  padding-bottom: 8px;
}

.tc-track { gap: 16px; }

/* justify-content MUST BE RESET, and this is not a nicety — without it
   the leading logos are permanently unreachable.

   `.logos` sets `justify-content: center` (FB-043c, so a ragged last row
   centers). Inherited onto a `nowrap` + `overflow-x: auto` rail whose
   content is wider than its box, centering distributes the overflow to
   BOTH sides — and `scrollLeft` cannot go below 0, so the part that
   overflows the START edge cannot be scrolled to, swiped to, or reached
   by a Previous button. On a 5-logo strip at lg that silently hides the
   first logo and a half. Overriding to flex-start puts the whole
   overflow at the end, where scrolling can reach it.

   The grid keeps its centering: this rule is scoped to the slider
   variant, so the two layouts differ only where they must. */
.logos--slider {
  justify-content: flex-start;
}

/* The slider is the grid row UNWRAPPED: every slide keeps the exact
   --m434-logo-basis a grid cell would have had, so a strip switched
   from Grid to Slider shows the same 2 / 3 / 4 logos across and simply
   scrolls past the rest instead of wrapping to a new row.

   flex-shrink MUST be 0 here. With `nowrap` a shrinkable basis would
   compress every logo into the container's width and leave nothing to
   scroll — the rail would look like a squashed grid. */
.logos--slider > a,
.logos--slider > img {
  flex: 0 0 var(--m434-logo-basis);
  scroll-snap-align: start;
}

.tc-item {
  flex: 0 0 86%;
  max-width: none;
  scroll-snap-align: center;
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin: 0;
  padding: 26px 24px;
  background: var(--white);
  box-shadow: var(--m434-rule-inset);
  text-align: left;
}

.tc-item blockquote {
  font-family: var(--display);
  font-weight: 700;
  font-size: 18px;
  line-height: 1.35;
  color: var(--m434-surface-ink);
  margin: 0;
}

/* margin-top:auto floors the byline so cards of different quote
   lengths still align their footers. */
.tc-item .cite {
  margin-top: auto;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  text-align: left;
}

/* .tc-nav is on the BUTTONS, not on a wrapper — the kit styled it as
   a flex CONTAINER (`display:flex; justify-content:center`), which
   both mislaid the arrows and defeated the `hidden` attribute the
   part renders them with, since an author `display` beats the UA's
   [hidden] rule. Hidden here is the base state; the lg + pointer
   query below re-enables only the buttons JS has actually unhidden,
   so [hidden] needs no rule of its own.

   The WRAPPER is `.tc-rail` — both parts now emit
   [prev] [track] [next] inside one — and it has no base rule on
   purpose: below lg it is a plain block, so the rail renders exactly
   as it did before that div existed. Touch keeps the swipe as its
   affordance and Isabel reported mobile as already good, which is the
   constraint the whole of FB-042 had to respect. */
.tc-nav { display: none; }

/* ============================================================
   sm — 480px. Byline rows fit; two-up card grids become viable.
   ============================================================ */
@media (min-width: 480px) {
  .cite {
    flex-direction: row;
    justify-content: center;
    gap: 16px;
    text-align: left;
  }

  .stats { grid-template-columns: 1fr 1fr; }
  .team-grid { grid-template-columns: 1fr 1fr; }

  /* The sm breakpoint exists for exactly this (D3 UPDATE's own
     description of 480px): "hero CTAs side by side". They do not
     always fit — "See Our Programs →" and "Request Assessment" at
     13px/0.1em need ~490px together, more than a 480px viewport has
     — so the row wraps, and flex-grow makes a wrapped button take its
     whole line instead of leaving a ragged half-width pill. Natural
     widths return at md, where the pair fits with room to spare. */
  .hero-actions {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 14px;
  }
  .hero-actions .btn {
    width: auto;
    flex: 1 1 auto;
  }
}

/* ============================================================
   md — 768px. Tablet: two-column card grids, split rows return,
   section heads become the design's flex lockups.
   ============================================================ */
@media (min-width: 768px) {
  .hero > .wrap { gap: 36px; }
  .hero-img,
  .ti-img { padding: 10px; }

  /* --- hero, tablet artboard (768) --- */
  .hero-copy .eyebrow { margin-bottom: 18px; }
  .hero-copy .lede { margin-top: 18px; }
  .hero-actions { margin-top: 30px; }
  .hero-actions .btn { min-height: 54px; padding: 0 28px; flex: 0 0 auto; }

  .hero--bg {
    min-height: 460px;
    padding: 64px 0 52px;
  }
  .hero--bg .hero-copy { max-width: 600px; }
  .hero--bg .eyebrow { letter-spacing: 0.2em; }
  .hero--bg .lede { max-width: 520px; }
  .hero-bg { object-position: 34% 42%; opacity: 0.46; }

  /* The band is wide enough for a left-anchored lockup now, so the
     scrim turns horizontal — the design's own 90deg wash, with the
     tablet stops (.92 → .62 at 70% → .4). */
  .hero-scrim {
    background: linear-gradient(
      90deg,
      rgba(17, 17, 17, 0.92) 0%,
      rgba(17, 17, 17, 0.62) 70%,
      rgba(17, 17, 17, 0.4) 100%
    );
  }

  /* §5.1 · H:52. Under reduced motion global.css clamps every
     animation to .001ms and one iteration, which lands this on its
     end frame and leaves a static grid — the behavior §4 asks for. */
  .hero-grid {
    display: block;
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
      linear-gradient(rgba(var(--m434-slate-rgb), 0.14) 1px, transparent 1px),
      linear-gradient(90deg, rgba(var(--m434-slate-rgb), 0.14) 1px, transparent 1px);
    background-size: 80px 80px;
    animation: m434-hero-grid 26s linear infinite;
  }

  .ti { gap: 36px; }

  /* The design's 90deg scrim (§5.5), now that the copy no longer
     spans the full band. */
  .cta:has(> .bg)::after {
    background: linear-gradient(
      90deg,
      rgba(17, 17, 17, 0.92) 0%,
      rgba(17, 17, 17, 0.55) 45%,
      rgba(17, 17, 17, 0.3) 100%
    );
  }

  .wrap > .check-list { grid-template-columns: 1fr 1fr; }
  .wrap > .check-list li { padding: 20px 24px; }

  /* The design's head row: eyebrow + title left, note right, baselines
     aligned on the bottom edge (§6 row 7). */
  .m434-steps-head {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
    gap: 30px;
    margin-bottom: 38px;
  }
  .m434-steps-head-r { max-width: 340px; }
  .m434-steps-title { font-size: 34px; }

  /* Two-up wrap. flex-basis of 50% minus the 1px hairline gap keeps
     exactly two cells per row; an odd last cell grows to the full
     width, which is the "3rd spans both" behavior §8.4 asks for.
     Because the basis is exactly half, :nth-child(odd) is always the
     LEFT column — the rule the chip logic below depends on. */
  .wsteps { flex-flow: row wrap; }
  .wstep {
    flex: 1 1 calc(50% - 1px);
    min-width: 0;
    gap: 12px;
    padding: 26px 22px;
  }
  .wstep-label { font-size: 34px; }
  .wstep-title { font-size: 19px; }

  /* 2x2: the chip returns to the vertical seam it was designed for —
     top:34px; right:-13px, un-rotated. The mobile design's own caption
     for this artboard is "2x2, CONNECTORS ONLY ON THE HORIZONTAL
     SEAM", and its data marks tabletChevron true on steps 01 and 03
     only: the left-hand cell of each row. A right-edge chip on an
     even (right-hand) cell would point at the grid's outer ring, and
     the 02 -> 03 hop is a line break, not a seam, so it gets none. */
  .wstep-ico {
    top: 34px;
    right: -13px;
    bottom: auto;
    left: auto;
    transform: none;
  }
  .wstep:nth-child(even) .wstep-ico { display: none; }

  .faq > summary { font-size: 19px; }

  .stat { padding: 30px 24px; }
  .stat-val { font-size: 40px; }

  .icon-cards-heading { margin-bottom: 36px; }
  .icon-cards-grid { grid-template-columns: 1fr 1fr; }
  .icon-card { padding: 30px 26px; }

  /* Three across — the basis carries that row's two gaps. §6 row 11's
     own 64px tile height arrives here too. */
  .logos { --m434-logo-basis: calc((100% - 2 * var(--m434-logo-gap)) / 3); }
  .logos > a,
  .logos > img { height: 64px; }

  /* The design's split row (§6 row 13). */
  .download-link {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    padding: 20px 24px;
  }

  .posts-feed-grid,
  .wrap:has(> .case-card) { grid-template-columns: 1fr 1fr; }

  .team-card { padding: 30px 26px; }
  .tc-photo { width: 112px; height: 112px; }

  .m434-quote-inner blockquote { font-size: 26px; }

  .tc-item {
    /* Two cards visible, halves minus half the 16px gap. */
    flex: 0 0 calc(50% - 8px);
    padding: 30px 28px;
  }
  .tc-item blockquote { font-size: 20px; }
}

/* ============================================================
   lg — 1024px. Desktop: the design's own column counts and the
   two-column hero / text+image splits.
   ============================================================ */
@media (min-width: 1024px) {
  /* §3.4: hero 1.05fr .95fr gap 56px. Held back to lg rather than
     md — a two-column hero at 768px leaves the h1 under 340px. */
  .hero > .wrap {
    grid-template-columns: 1.05fr 0.95fr;
    gap: 56px;
    align-items: center;
  }

  /* --- hero, desktop artboard (H:49-65) --- */
  .hero-copy .eyebrow { margin-bottom: 22px; }
  .hero-copy .lede { margin-top: 22px; }
  .hero-actions { margin-top: 34px; gap: 16px; }
  .hero-actions .btn { min-height: 56px; padding: 0 34px; }

  /* 600px is the design's `height`; min-height is deliberate — see the
     note on .hero--bg. With 48px of vertical padding inside a
     border-box min-height, the design's own copy still lands the band
     at exactly 600px, but longer copy opens it instead of being cut
     off by the band's overflow:hidden. */
  .hero--bg {
    min-height: 600px;
    padding: 48px 0;
  }
  .hero--bg .hero-copy { max-width: 900px; }
  .hero--bg .eyebrow { letter-spacing: 0.22em; }
  .hero-bg { object-position: 28% 40%; opacity: 0.5; }

  .hero-scrim {
    background: linear-gradient(
      90deg,
      rgba(17, 17, 17, 0.92) 0%,
      rgba(17, 17, 17, 0.55) 45%,
      rgba(17, 17, 17, 0.3) 100%
    );
  }

  .lede { font-size: 16px; max-width: 560px; }

  /* The band's lede stays at the design's 15px/1.6 — the .lede step to
     16px is for light body sections, and 16px over a photograph at
     560px runs long. */
  .hero--bg .lede { font-size: 15px; max-width: 560px; }

  /* §3.4: text+image .95fr 1.05fr gap 56px. image_side:left mirrors
     the grid, and only here — below lg the copy always leads. */
  .ti {
    grid-template-columns: 0.95fr 1.05fr;
    gap: 56px;
  }
  .ti--rev .ti-text { order: 2; }

  .m434-steps-title { font-size: 44px; }

  /* Equal-width N-up — the design's static grid shape (repeat(4,1fr)
     on the home page, repeat(3,1fr) in the block library).

     TSK-04455 item 15: the basis was `0`, and a flex item with basis 0
     NEVER wraps, so N steps always occupied one row no matter how many
     the repeater held. At 6-7 steps each title cell fell to ~107-133px
     against a 20px uppercase title — the reported cramping. A basis of
     `calc(25% - 1px)` caps the row at four and lets the rest wrap onto
     a second row, which is the same arithmetic the md rule already uses.

     This is a no-visual-change edit at <= 4 steps, which is every live
     instance today. */
  .wstep {
    flex: 1 1 calc(25% - 1px);
    gap: 14px;
    padding: 32px 26px;
  }
  .wstep-label { font-size: 36px; }
  .wstep-title { font-size: 20px; }

  /* ---------- Count-aware basis: six and nine steps go 3-up ----------
     FB-026, verbatim: "if there are six things in this block, can it
     make it 3x3 instead of 4x2?" Her "3x3" is loose — six across three
     columns is 3 wide by 2 deep — but the intent is not: even rows, no
     orphan row.

     The 25% basis above caps the row at four, and that is count-blind.
     Because the basis carries flex-grow: 1, the leftover cards do not
     just sit in a short row — they GROW to fill it, so six steps render
     as four narrow cards above two double-width ones, and the block
     looks like two different components stacked. 6 -> 3+3 and 9 -> 3+3+3
     are the two counts a 33.33% basis makes even.

     WHY 6 AND 9 ARE ENUMERATED rather than tested as "a multiple of 3":
     12 is a multiple of 3 AND of 4, and at 4-up it is already three even
     rows in the design's own shape, so reflowing it would be a loss. The
     honest test is "multiple of three, not of four", which has no
     compact selector form; the repeater realistically never holds more
     than nine, so the two counts are written out. 3 steps need no entry
     — flex-grow already spreads three cards to a third each.

     5 and 7 are deliberately untouched: neither has an even layout at
     any column count, so there is nothing to reflow them to.

     :has() is guarded the way program-cards.css:298 and archive.css:143
     guard theirs. Without :has() the block keeps today's 4-up wrap,
     which is a shape, not a break. */
  @supports selector(:has(*)) {
    .wsteps:has(> .wstep:nth-child(6):last-child,
                > .wstep:nth-child(9):last-child) > .wstep {
      flex-basis: calc(33.3333% - 1px);
    }
  }

  /* ---------- The connector chips, per row width ----------
     A chip is correct on a card that has a neighbor across a VERTICAL
     seam, and wrong on any card that ends a row — there the chip hangs
     off the outer edge and points into empty space. top:40px is the
     desktop offset (H:120) — the chip centers on the 36px numeral.

     THE 4-UP CASE. The md rule suppressed every even cell (2x2 there
     puts a row end on every even card); at lg with four per row only
     every FOURTH card ends a row, so the suppression is lifted for the
     even cards that are not 4n. The `:not(:nth-child(4n))` is what
     repairs FB-026's other half:

       the rule used to read `.wstep:nth-child(even):not(:last-child)`
       (0,4,0) — `:not()` takes its argument's specificity — paired with
       a separate `.wstep:nth-child(4n)` suppressor at only (0,3,0),
       which therefore NEVER won the conflict it was written to win. The
       comment claimed the two tied at (0,4,0) and were resolved by
       order; they did not tie, so card 4's chip has been VISIBLE and
       hanging off the right edge of the grid on every 5-, 7-, 8- and
       9-step instance at lg. Narrowing the show rule removes the
       conflict instead of re-tuning it, and the explicit 4n suppressor
       below is kept, now at a specificity that cannot lose, so the lg
       intent is readable without cross-referencing the md tier.

     Repairing it newly HIDES card 4's chip on those instances. That is
     the correct outcome and it is a visible change, not a no-op. Inert
     at <= 4 steps, where nth-child(4n) IS :last-child. */
  .wstep-ico { top: 40px; }
  .wstep:nth-child(even):not(:last-child):not(:nth-child(4n)) .wstep-ico { display: flex; }
  .wsteps > .wstep:nth-child(4n) .wstep-ico { display: none; }

  /* THE 3-UP CASE inverts which cards end a row: 3, 6 and 9 do, and card
     4 now STARTS row 2 and does have a right-hand neighbor. So both
     rules above are wrong here and both are restated, scoped to the same
     two counts as the basis query.

     Kept as two rules over disjoint sets rather than one rule with an
     override: the show rule is (0,8,0) and the hide (0,7,0) — the
     `:has()` contributes the (0,3,0) of its most specific argument — so
     each beats every rule above it on the cards it claims, and neither
     depends on declaration order. Card 6 of a six-step instance is
     covered twice (3n here, :last-child at :825); both say none. */
  @supports selector(:has(*)) {
    .wsteps:has(> .wstep:nth-child(6):last-child,
                > .wstep:nth-child(9):last-child)
      > .wstep:not(:nth-child(3n)):not(:last-child) .wstep-ico {
      display: flex;
    }
    .wsteps:has(> .wstep:nth-child(6):last-child,
                > .wstep:nth-child(9):last-child)
      > .wstep:nth-child(3n) .wstep-ico {
      display: none;
    }
  }

  .stats { grid-template-columns: repeat(4, 1fr); }
  .stat { padding: 36px 26px; }
  .stat-val { font-size: 66px; }

  .icon-cards-grid { grid-template-columns: repeat(3, 1fr); }

  /* THE :has() QUANTITY QUERIES THAT USED TO SIT HERE ARE STILL GONE FOR
     ICON CARDS — but the policy the old note argued from has changed, so
     read both halves before restoring or deleting anything.

     STILL TRUE. 8ff15df removed them because they reshaped the track
     count for 1, 2 and 4 cards so no cell was left empty — a fix aimed
     at the gray block an empty cell painted, not at the count itself.
     The per-card ring above removes that gray for EVERY count, including
     the 5 and 7 the queries never covered, so here the special cases buy
     nothing but a different shape: a lone card stretched to full width,
     four cards forced to 2x2. They stay deleted.

     NO LONGER TRUE. The sentence that used to close this note — "an
     editor should not have to learn that this block reflows while those
     two do not" — was overtaken by FB-026: "if there are six things in
     this block, can it make it 3x3 instead of 4x2?", promoted and
     approved for process_steps, whose 3-up query now sits a little way
     above. The policy is therefore no longer "no grid reflows by count"
     but "reflow only where a short row cannot simply end".

     THAT LINE IS DRAWN BY THE LAYOUT MECHANISM, NOT BY TASTE, which is
     what keeps it teachable:
       · icon cards, and the team and program grids, are CSS grids on
         fixed 1fr tracks. A short row ends after its last card and every
         card keeps its width. There is nothing left to fix once the
         gray is gone, so no quantity query.
       · .wsteps (:756) is a FLEX row with flex-grow: 1. A short row
         CANNOT simply end — its cards grow to fill the width, so six
         steps draw as four narrow cards above two double-width ones.
         Only a count-aware basis makes those rows even.
     The two idioms disagree because the two layouts do, and the editor
     is left with one consistent promise: rows stay even. */

  .icon-card { padding: 34px 28px; }
  .icon-card-title { font-size: 20px; }

  /* §6 row 11's four-up, as a basis carrying that row's three gaps. */
  .logos { --m434-logo-basis: calc((100% - 3 * var(--m434-logo-gap)) / 4); }

  .posts-feed-grid,
  .wrap:has(> .case-card) { grid-template-columns: repeat(3, 1fr); }
  .case-card h3,
  .dark-bg .case-card h3 { font-size: 19px; }

  .team-grid { grid-template-columns: repeat(3, 1fr); }

  /* Scoped to .m434-quote-inner — the testimonial band's own figure —
     rather than to `.quote`. `.tc-item` also carries the class `quote`
     (`<figure class="quote tc-item">`), and this rule is declared AFTER
     `.tc-item blockquote { font-size: 20px }` at equal (0,1,1)
     specificity, so unscoped it won the tie and painted the CAROUSEL
     card's pull-quote at 30px from 1024 up — measured on the /blocks/
     library instance. L:234 draws that card at 22px. The 1200 step
     below is scoped the same way and for the same reason. */
  .m434-quote-inner blockquote { font-size: 30px; }

  /* ---------- The program title lockup goes two-up (FB-023) ----------
     The base tier is deliberately unstyled — the note above
     .m434-program-figure (line 2855) carries the reasoning and the
     no-regression argument. Only the desktop row lives here.

     HELD TO lg, NOT md, BY ARITHMETIC. The split must not take the copy
     below the 46ch the lede is capped to, which is 548px at 18px
     Montserrat (its '0' advance is 0.662em). .wrap.wrap-narrow is
     border-box, so its content box is 900px minus two gutters: 836px at
     md's 32px gutter, 812px at lg's 44px. At md a usable image column
     would cut the copy under that measure and start re-wrapping the
     excerpt; at lg, 220 + 32 leaves 560px — 12px clear of the cap — so
     the excerpt's line breaks are the same as on the single-column
     page. 220px is single-staff.php's own desktop portrait width,
     reused rather than re-picked, and it is the "small size" the request
     asks for.

     WHAT IT COSTS THE <h1>, measured rather than guessed (Jost 700 —
     the self-hosted face nearly every visitor sees — at the lg step of
     52px with -0.01em tracking, against the three live program titles):
     at 812px all three set to two lines. At 560px "Smart Manufacturing
     Roadshows" and "Manufacturing Performance Diagnostics™" go to
     three, and "Advanced Manufacturing Apprenticeship Program" to four.
     NO FIGURE WIDTH IN THE USABLE RANGE AVOIDS THAT: holding the
     two-line wrap needs 805px of copy, which leaves 7px for an image.
     The longest single word, MANUFACTURING, is 458px, so a 560px column
     cannot be overflowed by an unbreakable word either.

     min-width: 0 on the text item: without it a flex item's
     content-based automatic minimum can hold the row wider than its
     container and the page scrolls sideways — the same failure the
     .tc-rail note below records. */
  /* align-items: stretch, NOT flex-start, and that is the whole revision.
     The client's note was "it should be bigger... likely the same height
     as the two combined, or at least as the intro text". Stretch makes
     both items take the ROW's height, and the row takes the height of
     its tallest item — so the figure is exactly as tall as the lede
     column and the two always agree, whatever the excerpt length.
     `align-items: flex-start` is what made the first pass look, in the
     client's word, silly: the figure shrank to its own intrinsic height
     next to a much taller block of copy. */
  .m434-program-lockup {
    display: flex;
    align-items: stretch;
    gap: 32px;
  }

  .m434-program-ident {
    flex: 1 1 auto;
    min-width: 0;
  }

  /* The figure's 28px is the single-column stack's rhythm; in the row
     the copy sets the height, so it comes off.

     SELECTED VIA THE PARENT ON PURPOSE — `.m434-program-lockup >` is not
     decoration. The base rule that sets `margin: 0 0 28px` lives at the
     foot of this file (the .m434-program-* group), i.e. AFTER this media
     query, and a media query contributes no specificity. A bare
     `.m434-program-figure { margin: 0 }` here is therefore (0,1,0)
     against an identical (0,1,0) declared later, and loses — the figure
     kept its 28px bottom margin at lg, which with `align-items: stretch`
     made the image 28px shorter than the copy beside it and quietly
     defeated the height match this row exists for. The child combinator
     takes it to (0,2,0) so source order stops mattering.

     460px, up from 220px. The container is now .wrap (1200px, matching
     the body below) rather than .wrap-narrow (900px), and the <h1> has
     moved out of this row to span both columns — so the row is the lede
     plus the figure, and the lede's own 46ch measure (~548px) leaves
     this much beside it without crowding it. Kept as a fixed basis
     rather than a percentage so the lede never drops below its measure
     at the 1024px breakpoint itself. `sizes` in single-program.php is
     declared as 460px to match; the two must move together. */
  .m434-program-lockup > .m434-program-figure {
    flex: 0 0 460px;
    margin: 0;
  }

  /* FILL THE STRETCHED FIGURE. `height: 100%` is what converts the
     parent's stretch into a real height on the replaced element, and
     min-height is the floor for the case the row cannot supply one — a
     one-line excerpt would otherwise stretch the figure to about 30px.
     With the floor, a short excerpt makes the IMAGE the tallest item
     and the row grows to it instead, which is the intent.

     NO add_image_size() AND NO REGISTERED CROP: object-fit does the
     cropping at paint time, so nothing has to be regenerated across the
     existing media library. object-fit IS NOT OPTIONAL alongside a
     forced height — `width: 100%` plus a set height squashes the image
     rather than cropping it, so the pair is written together or not at
     all. No aspect-ratio here on purpose: with both width and height
     resolved it could never apply, and a rule that cannot fire is worse
     than no rule because the next reader trusts it. */
  .m434-program-hero {
    width: 100%;
    height: 100%;
    min-height: 300px;
    object-fit: cover;
  }
}

/* ---------- The arrow row (desktop + pointer only) ----------
   The arrows exist only where a pointer can reach them; on touch the
   swipe IS the affordance, and the design ships none at all (§6 row 10
   / §10.5 — overruled, see the rail section head).

   THEY ARE NO LONGER AN OVERLAY, and that overlay was FB-042's "kinda
   jank on desktop". As shipped they were `position: absolute; top: 50%`
   against `.wrap`, which produced three faults at once:
     · `top: 50%` centered them on the WRAP, whose box includes the
       eyebrow and the h2, so on any instance with a heading — which is
       both of the multi-item carousels stored on /blocks/ — the arrows
       sat well below the middle of the track they steer;
     · `left/right: 0` resolves to the wrap's PADDING edge, so a 44px
       button overhung the track by 44px minus the gutter and the first
       and last cards slid underneath it. There was no padding between a
       button and the side of the slider at all, which is what the
       client actually asked for;
     · out of flow, nothing RESERVED room for them, so no amount of
       nudging a margin could have produced that padding.
   So the row is laid out instead of positioned: both parts now wrap
   prev + track + next in `.tc-rail`, which becomes a flex row here.
   The 20px `gap` IS the padding between each button and the edge of
   the slider, each button is a `flex: 0 0 44px` lane the track cannot
   grow into, and `align-items: center` centers the arrows on the TRACK
   rather than on the section head. Nothing overlaps, so the focus ring
   the global `:focus-visible` draws (2px + 2px offset) now lands in the
   gap instead of on a card. */
@media (min-width: 1024px) and (hover: hover) {
  .tc-rail {
    display: flex;
    align-items: center;
    gap: 20px;
  }

  /* min-width: 0 lets the track shrink inside the row. Without it the
     flex item's content-based automatic minimum keeps the rail wider
     than its container and the SECTION scrolls sideways instead of the
     rail — the classic overflowing-flex-child failure. */
  .tc-rail > .tc-track,
  .tc-rail > .logos--slider {
    flex: 1 1 auto;
    min-width: 0;
  }

  .tc-nav:not([hidden]) {
    /* D3's 44x44 floor on every icon control. The lane IS the button,
       so the hit area is the whole 44px square rather than the glyph. */
    flex: 0 0 44px;
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 0;
    background: var(--white);
    box-shadow: var(--m434-rule-inset);
    color: var(--ink);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
  }

  /* Scoped away from the end state so an arrow with nothing left to
     scroll to does not light up under the pointer as though it had. */
  .tc-nav:not([aria-disabled="true"]):hover {
    color: var(--m434-accent-text);
    box-shadow: inset 0 0 0 1px var(--red);
  }

  /* THE END STATE IS aria-disabled, NOT the `disabled` attribute, and
     testimonial-carousel.js sets it that way on purpose: a disabled
     button leaves the tab order, so pressing Next until the rail ran
     out destroyed the focus that was sitting on it and dropped a
     keyboard user back at the document. aria-disabled keeps the control
     focusable and announced as unavailable, and the script makes the
     click a no-op. 1.4.3 and 1.4.11 both exempt an inactive control
     from their contrast floors, which is what buys the fade. */
  .tc-nav[aria-disabled="true"] {
    opacity: 0.35;
    cursor: default;
  }

  /* THE SCROLLBAR — and the honest limit of what CSS can do about it
     (FB-042b). Where the arrows are present they ARE the affordance, so
     the permanent bar under the rail is noise and comes off:
     `scrollbar-width: none` plus the WebKit pseudo-element covers every
     engine that ships today, and the 8px of clearance the bar needed
     goes with it.

     What CSS CANNOT express is "only while scrolling" — there is no
     is-scrolling state to select on, and toggling `scrollbar-width`
     from a scroll listener resizes the scrollport mid-gesture, so it
     was deliberately not built. Overlay scrollbars (macOS, iOS,
     Android, and Windows 11 in some engines) already auto-hide
     natively; a classic-scrollbar browser does not, which is exactly
     why this is scoped to the one plane where a REPLACEMENT affordance
     exists. Below lg, and on any pointer without hover, the bar stays
     visible on purpose: that is the only cue left that the rail
     scrolls.

     `.tc-rail--nav` is added by the script once it has actually
     unhidden the buttons, so a no-JS page keeps its scrollbar rather
     than losing every scroll cue at once. */
  .tc-rail--nav > .tc-track,
  .tc-rail--nav > .logos--slider {
    scrollbar-width: none;
    padding-bottom: 0;
  }
  .tc-rail--nav > .tc-track::-webkit-scrollbar,
  .tc-rail--nav > .logos--slider::-webkit-scrollbar {
    display: none;
  }
}

/* ============================================================
   xl — 1200px. The container is capped; only the largest display
   step remains (§8.3).
   ============================================================ */
@media (min-width: 1200px) {
  /* Scoped to the BAND variant on purpose, two ways. (1) The photo
     artboard tops out at 30px (H:264) and the crimson one at 32px
     (L:210), so this is the only variant the step belongs to.
     (2) `.tc-item` also carries the class `quote`, so the unscoped
     form reached the carousel card and overrode its own 20px lg rule
     at equal specificity by source order — a 32px pull-quote in a
     card, at 1200 and up only. */
  .m434-quote--band .m434-quote-inner blockquote { font-size: 32px; }
}

/* ---------- Text + image: the technology tag row ----------
   design-system.md's About panel (home H:92-101). Rendered on the home
   page but unstyled until now, so the seven technologies came out as a
   browser-default disc bullet list.

   Chips, not bullets: `list-style: none` plus an accent square drawn as
   a ::before, which keeps the marker inside the flex row where a real
   list marker would sit outside it and break the wrap. */
.m434-ti-list { margin-top: 26px; }

.m434-ti-list-label {
  font-family: var(--body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  /* --m434-slate-mute is a flat #6b7280: 4.43:1 on --bg-soft, 4.26:1
     on --bg-cool and 3.13:1 on --navy, all under 4.5:1 at 11px. */
  color: var(--m434-muted-ink);
  margin: 0 0 14px;
}

.m434-ti-list-items {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 28px;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* --m434-surface-ink: a hardcoded --ink is 1.15:1 on --navy and
   1.51:1 on crimson, i.e. all but invisible on the dark bands
   text_image can be set to. */
.m434-ti-list-item {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--body);
  font-size: 14px;
  line-height: 1.4;
  color: var(--m434-surface-ink);
}

/* 6px accent square. Kept at every size — DECISIONS D3 lists the accent
   marks among the motifs that carry the industrial language. */
.m434-ti-list-item::before {
  content: "";
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  background: var(--red);
}

/* ============================================================
   Single program — single-program.php
   ------------------------------------------------------------
   THIS LIVES IN components.css ON PURPOSE, not in program-cards.css
   where the rest of the .m434-program-* family lives. Verified on the
   live install: /programs-services/<slug>/ loads exactly
   fonts · tokens · global · components · blocks · forms · search.
   program-cards.css is a PER-BLOCK asset (inc/enqueue.php's
   $block_assets, gated on m434_view_has_block('program_cards')) and
   archive.css is a PER-PAGE one (gated on is_home/is_archive/is_404/
   is_singular('post')) — a program single satisfies neither, so a
   rule placed in either file would never load on the only URL that
   emits these classes. components.css is enqueued sitewide.

   All three classes below rendered with ZERO matching rules before
   this block: found by cross-checking every class in the rendered
   markup of every public URL against the selectors in the stylesheets
   that URL actually loads.
   ============================================================ */

/* The program lede. Measured unstyled it computed 15px/24px — the
   plain `body` inheritance — directly above a `.prose` running
   16px/27.2px, i.e. the introduction was SMALLER than the body it
   introduces. 18px/1.6 + --m434-slate-strong is not a new decision:
   it is exactly rich-text.css's `.m434-rt--lead .prose > p:first-of-type`,
   which is the same role (a promoted lede paragraph above body prose),
   so the two render identically.

   --m434-slate-strong #4b5563, flat rather than --m434-muted-ink: the
   template hardcodes `<article class="section">` with no bg_color
   field, so this paragraph's ground is the body's --white, always.
   7.56:1 there (and 6.93:1 on --bg-soft if the wrap is ever tinted).
   If single-program.php ever grows section settings, swap this one
   token for --m434-muted-ink and nothing else needs to change. */
.m434-program-lede {
  font-family: var(--body);
  font-size: 18px;
  line-height: 1.6;
  color: var(--m434-slate-strong);
  max-width: 46ch;
  margin: 16px 0 26px;
}

/* ---- The title lockup (FB-023) ----
   "Can we make it so that adding a featured image on Program CPTs adds
   an image in a small size to the right of the excerpt to fill that
   empty space?" The lede's 46ch cap above is the reading measure and
   stays; what it never had was a partner in the other half of the row.

   THERE IS DELIBERATELY NO BASE-TIER RULE FOR .m434-program-lockup OR
   .m434-program-ident, and that is the whole no-regression argument.
   Below lg both are plain block boxes, which IS the single-column
   stack: they add no padding, border or margin, so the eyebrow / h1 /
   lede / figure order and every collapsed margin between them are
   identical to the markup this replaced. A program with no featured
   image emits no <figure> at all, so its text column does not narrow
   and its rhythm does not move at any width — which is every live
   program today. The two-column half is in the lg query at line 2562,
   with the rest of this build's desktop shapes.

   The shape itself is single-staff.php's .m434-staff-lockup
   (team-cards.css:308), not a new pattern, and the text div is first in
   the DOM with the figure second — so no `order` property appears here
   or there. See the render-site comment in single-program.php. */

/* The lead image's frame — the same shape as archive.css's
   .m434-post-figure: the hairline this theme uses for a lead figure
   rather than a drop shadow, since the design has no elevation on light
   grounds.

   THE MARGIN IS NOT OPTIONAL. A <figure> carries a UA margin of
   `1em 40px` and nothing in global.css resets it, so leaving this unset
   would indent the image 40px from the text above it. The 28px is the
   value that used to sit on the <img> itself, moved out one level so
   the row at lg can drop it in one place. */
.m434-program-figure {
  margin: 0 0 28px;
}

/* The lead image. The hairline ring stays on the <img> rather than
   moving to the <figure> so that it hugs the actual pixels at lg, where
   `object-fit: cover` crops the painted image inside a box whose height
   the row dictates — a ring on the <figure> would trace the box instead
   of the photograph.

   `display: block` kills the line-box descender — an inline <img> is a
   text-level box, so ~5px of phantom space would otherwise sit between
   the bottom of the photo and the bottom of its <figure>. Same
   declaration, for the same reason, as archive.css's
   `.m434-post-figure img`.

   Only rendered when the program has a featured image. That case is now
   exercised — the client attached one to /smart-manufacturing-roadshows/
   on 2026-09-08, which is what produced the sizing revision in the lg
   block above. */
.m434-program-hero {
  display: block;
  width: 100%;
  box-shadow: var(--m434-rule-inset);
}

/* The "All programs" back link. Measured unstyled, the gap between the
   end of .prose and this paragraph was 0.0px — the affordance sat
   flush against the last line of the article, reading as part of it.
   Hairline + rhythm above, matching the two other end-of-article
   affordances in the theme (.m434-pagination, .m434-post-nav).
   The link inside is .m434-arrowlink, which global.css owns and which
   already carries its own 44px hit target and focus ring. */
.m434-program-back {
  margin: 32px 0 0;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}
