/*
Theme Name: San Diego
Theme URI: https://systemprompt.co/
Author: System Prompt
Author URI: https://systemprompt.co/
Description: A fast, builder-free WordPress theme for schools. Full-screen hero slideshow, program tiles, core values band, testimonial voices, fast facts, news, and a rich footer -- every section editable from the dashboard by non-technical staff, with per-section show/hide, plain-English settings, and brand colors that recolor the whole site from the Customizer.
Version: 1.1.0
Requires at least: 6.0
Tested up to: 6.7
Requires PHP: 7.4
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: cristo-rey
Tags: education, one-column, custom-menu, custom-logo, featured-images, translation-ready
*/

/* ==========================================================================
   Tokens
   ========================================================================== */
:root {
  --c-primary: #004a98;      /* Cristo Rey blue */
  --c-primary-dark: #00376f;
  --c-secondary: var(--c-primary); /* overridable via Customizer/theme mod; falls back to primary so the real theme is unchanged by default */
  --c-navy: #002b57;         /* deep footer navy */
  --c-accent: #ffcc00;       /* Cristo Rey gold */
  --c-ink: #262626;
  --c-gray: #525252;
  --c-gray-light: #a3a3a3;
  --c-line: #e5e5e5;
  --c-bg-light: #f5f5f5;
  --c-white: #ffffff;
  --font-body: "Poppins", -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-display: "Anton", "Arial Narrow", Impact, sans-serif;
  --container: 1240px;
  --header-h: 84px; /* actual rendered height of .site-header__bar = the logo cap (56px) plus the brand block's 14px padding top and bottom. Brian asked for a shorter bar on 2026-08-27, down from 106px, once the header became solid on every page. It is also the top-offset every page applies to #main, so it and `.site-header__brand` must move together */
}

/* ==========================================================================
   Base
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  .logo-strip__track { animation: none !important; }
}
body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.75;
  color: var(--c-ink);
  background: var(--c-white);
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: var(--c-primary); }
a:hover { color: var(--c-primary-dark); }

h1, h2, h3, h4, h5, h6 { line-height: 1.15; margin: 0 0 0.5em; color: var(--c-ink); }
h1, h2 { font-family: var(--font-display); font-weight: 400; letter-spacing: 0.01em; text-transform: uppercase; }
h1 { font-size: clamp(2.4rem, 5vw, 3.6rem); }
h2 { font-size: clamp(2rem, 4vw, 3rem); }
h3 { font-size: 1.5rem; font-weight: 600; }
h4 { font-size: 1.2rem; font-weight: 600; }

.screen-reader-text {
  border: 0; clip: rect(1px,1px,1px,1px); clip-path: inset(50%);
  height: 1px; width: 1px; margin: -1px; overflow: hidden;
  padding: 0; position: absolute; word-wrap: normal !important;
}
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--c-accent); color: var(--c-ink); padding: 10px 18px;
  font-weight: 600; text-decoration: none;
}
.skip-link:focus { left: 0; }

.container { max-width: var(--container); margin: 0 auto; padding: 0 24px; }

/* Section scaffolding shared across the front page */
.section { padding: 88px 0; }
.section--light { background: var(--c-bg-light); }
.section--pinstripe {
  background-color: #f7f7f7;
  background-image: repeating-linear-gradient(135deg, rgba(0,0,0,0.045) 0 1px, transparent 1px 9px);
}
.eyebrow {
  display: block;
  font-size: 0.95rem; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--c-gray);
  margin-bottom: 2px;
}
.display-heading {
  font-family: var(--font-display);
  text-transform: uppercase;
  color: var(--c-primary);
  font-size: clamp(2.6rem, 6.5vw, 4.6rem);
  line-height: 1;
  margin: 0 0 18px;
}
.section-intro { max-width: 720px; }

.btn {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 0.85rem; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 14px 30px;
  border: 2px solid var(--c-primary);
  color: var(--c-primary);
  background: transparent;
  text-decoration: none;
  transition: background 0.2s, color 0.2s;
  cursor: pointer;
}
.btn:hover, .btn:focus { background: var(--c-primary); color: var(--c-white); }
.btn--solid { background: var(--c-primary); color: var(--c-white); }
.btn--solid:hover, .btn--solid:focus { background: var(--c-primary-dark); }
.btn--gold { border-color: var(--c-accent); background: var(--c-accent); color: var(--c-ink); }
.btn--gold:hover, .btn--gold:focus { background: #e6b800; border-color: #e6b800; color: var(--c-ink); }

.arrow-btn {
  width: 52px; height: 52px; border-radius: 50%;
  border: 0; background: var(--c-primary); color: var(--c-white);
  font-size: 1.1rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; transition: background 0.2s;
}
.arrow-btn:hover, .arrow-btn:focus { background: var(--c-primary-dark); }

/* ==========================================================================
   Header
   ========================================================================== */
/* ONE SOLID BAR, ALWAYS, ON EVERY PAGE. Two calls from Brian on
   2026-08-27: "the top header should always be there not just on scroll"
   and "the header background should be the same color on everything
   except the search and hamburger menu, also make it smaller as it's too
   big now."

   It used to start transparent over the homepage hero and swap to white
   only once an IntersectionObserver saw the hero leave the viewport
   (`body.header-scrolled`). That observer and the class are deleted from
   main.js and from this file rather than overridden -- an orphan toggle
   nothing sets is worse than no toggle.

   The background now lives HERE, on the bar, not on the two blocks inside
   it, so there is one uninterrupted colour behind the logo and the nav
   alike. `.search-toggle` and `.menu-toggle` are the only two things
   inside it that keep a colour of their own, which is what Brian asked
   for. Deliberately NOT matched to bdcs.org: their header is 212px and
   transparent over the hero. This one is 84px and opaque, because that
   is what he wants.

   Because the bar is opaque on the homepage too, `#main` takes the
   `--header-h` offset on every page (below) and the hero's min-height is
   reduced by the same amount, so the first screen is still exactly one
   viewport and nothing jumps on load. */
.site-header {
  position: fixed; top: 0; left: 0; right: 0;
  z-index: 50;
  background: var(--c-white);
  border-bottom: 1px solid var(--c-line);
  box-shadow: 0 2px 10px rgba(0,0,0,0.10);
}
.site-header__bar {
  display: flex; align-items: stretch; justify-content: space-between;
  min-height: var(--header-h);
}
/* The white fill, the drop shadow and the angled `clip-path` all existed
   to make this plaque read as a card floating on the hero photo. Against
   a bar that is now solid on every page they do the opposite: the fill is
   white on white, the clip cuts an invisible notch, and the shadow stacks
   against the header's own, leaving a heavier smudge under the logo than
   under the middle of the bar. That mismatch is exactly what Brian was
   pointing at. All three removed; the bar owns the background. */
.site-header__brand {
  padding: 14px 26px;
  display: flex; align-items: center;
}
/* 56 + 14 + 14 = the 84px bar exactly. Change one and change the other,
   and `--header-h` with them. */
.site-header__brand img { max-height: 56px; width: auto; }
.site-header__brand .site-title {
  font-family: var(--font-display); font-size: 1.5rem; margin: 0;
  color: var(--c-primary); text-transform: uppercase; text-decoration: none;
}
/* FIXED 2026-08-27. This block used to carry `align-self: flex-start`,
   which opted it out of the bar's `align-items: stretch` and left it
   53px tall inside a 106px header while the logo plaque next to it filled
   the full 106px. The result was a white slab with dead white space under
   it -- most obvious in the scrolled state, where the whole bar goes
   white and the block's own drop shadow reads as a stray horizontal
   seam. `align-items: stretch` now runs the nav, the search button and
   the hamburger the full height of the bar, so the right-hand block
   matches the brand plaque exactly.

   Not copied from bdcs.org on purpose: their `.right-container` is 212px
   (their full header height) but is transparent, and the white bar inside
   it is only 75px and top-aligned. That reads fine over a 212px header;
   it does not over ours at half the height. Brian asked for the block to
   fill, so it fills. Header height itself is unchanged at 106px, so
   `--header-h` and the inner-page top offset need no adjustment. */
.site-header__right {
  display: flex; align-items: stretch; gap: 0;
}
.utility-nav { display: flex; }
.utility-nav ul { list-style: none; display: flex; margin: 0; padding: 0; }
.utility-nav li { display: flex; }
/* Vertical padding became height, so the label centres in whatever the
   bar is; the horizontal 16px is unchanged and still sets the hit area
   and the width of the gold underline below. */
.utility-nav a {
  position: relative;
  display: flex; align-items: center; padding: 0 16px;
  font-size: 0.82rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; text-decoration: none; color: var(--c-ink);
}
/* NO POSITIONAL COLOUR. This used to be
   `.utility-nav li:first-child a { color: var(--c-primary) }`, ported from
   bdcs.org where "Summer Camp" renders blue -- but on their site that is a
   featured seasonal promo, not a state, and copying it as a `:first-child`
   rule painted whatever link happened to sit in position one. Brian,
   2026-08-27: "they should be the same unless selected, but even then it
   would take you to a new page." Every utility link now shares one resting
   colour.

   Current page is signalled by the gold underline below -- an addition on
   top of a consistent base colour, driven by the page the visitor is
   actually on (`aria-current="page"`, which WordPress's nav walker sets,
   with `current-menu-item` as a belt-and-braces fallback for menus built
   by hand), never by position.

   If the school ever does want one link promoted, `.utility-nav .is-featured`
   is the opt-in for it: a class they choose to apply, not a rule that
   guesses. */
.utility-nav .is-featured > a { color: var(--c-primary); }
.utility-nav a:hover { color: var(--c-primary); }
.utility-nav a::after {
  content: "";
  position: absolute; left: 16px; right: 16px; bottom: 0;
  height: 3px; background: var(--c-accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.2s ease;
}
.utility-nav a:hover::after,
.utility-nav a:focus-visible::after,
.utility-nav a[aria-current="page"]::after,
.utility-nav .current-menu-item > a::after { transform: scaleX(1); }
/* `height: auto` + the parent's stretch, NOT a fixed 52px: these two are
   solid colour blocks, and leaving them short would put the same white
   gap back under them that the fix above just removed. */
.search-toggle, .menu-toggle {
  border: 0; cursor: pointer; height: auto; align-self: stretch;
  display: inline-flex; align-items: center; justify-content: center;
}
/* Translate toggle: sits with the utility links, dressed like one. */
.translate-toggle {
  border: 0; background: transparent; cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font-body); font-size: 13px; font-weight: 600;
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--c-ink); padding: 0 14px; align-self: stretch;
  transition: color 0.3s ease;
}
.translate-toggle:hover, .translate-toggle:focus-visible { color: var(--c-primary); }
/* Google's translate bar tries to push the page down; the fixed header
   already owns the top of the viewport, so pin the body back. */
body { top: 0 !important; }
.skiptranslate iframe { display: none !important; }
.search-toggle { width: 52px; background: var(--c-bg-light); color: var(--c-ink); }
.menu-toggle { width: 60px; background: var(--c-primary); color: var(--c-white); }
/* Hover label, per bdcs.org's hamburger (their button turns the secondary
   color and the word "Menu" appears -- 16px white, 0.3s). Absolutely
   positioned so the header never shifts; also shown on keyboard focus. */
.search-toggle, .menu-toggle { position: relative; overflow: hidden; }
.toggle-label {
  position: absolute; left: 0; right: 0; bottom: 5px;
  font-style: normal; font-size: 10px; line-height: 1; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; text-align: center;
  opacity: 0; transform: translateY(6px);
  transition: opacity 0.3s ease, transform 0.3s ease;
  pointer-events: none;
}
.search-toggle:hover, .search-toggle:focus-visible { background: var(--c-line); }
.menu-toggle:hover, .menu-toggle:focus-visible { background: var(--c-secondary); }
.search-toggle:hover .toggle-label, .search-toggle:focus-visible .toggle-label,
.menu-toggle:hover .toggle-label, .menu-toggle:focus-visible .toggle-label {
  opacity: 1; transform: translateY(0);
}
.search-toggle:hover svg, .search-toggle:focus-visible svg,
.menu-toggle:hover .bars, .menu-toggle:focus-visible .bars {
  transform: translateY(-5px);
}
.search-toggle svg, .menu-toggle .bars { transition: transform 0.3s ease; }
.menu-toggle .bars { display: inline-block; width: 22px; }
.menu-toggle .bars span {
  display: block; height: 2px; background: currentColor; margin: 5px 0;
  transition: transform 0.2s, opacity 0.2s;
}
body.menu-open .menu-toggle .bars span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
body.menu-open .menu-toggle .bars span:nth-child(2) { opacity: 0; }
body.menu-open .menu-toggle .bars span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* The header is `position: fixed` and opaque everywhere, so every page --
   the homepage included, as of 2026-08-27 -- pushes its own content clear
   of it by exactly the header height. This used to read `body:not(.home)`,
   from when the homepage hero was designed to sit UNDER a transparent bar.
   A stale `--header-h` now overlaps or gaps content on EVERY page rather
   than just the inner ones, so that token and the brand padding above are
   a matched pair. */
#main { padding-top: var(--header-h); }

/* --------------------------------------------------------------------------
   Menu overlay -- right-side slide-in panel, measured on bdcs.org
   2026-08-27 at 1324x830 (Brian: "our hamburger menu looks nothing like
   bdcs.org"). Their geometry: navy panel over the right ~654px, page still
   visible left; top-level links Anton 38/49 uppercase, right-aligned
   against a 1px vertical divider; child links Poppins 14/21 in the column
   right of the divider, revealed per hovered/focused section; 46px white
   circle close with a secondary-color x; a secondary-color action bar
   pinned to the panel bottom with two icon CTAs. The slide-in duration is
   ASSUMED at 0.45s ease-out (their transition was not sampleable
   mid-flight); everything geometric above is measured. */
.menu-overlay {
  position: fixed; inset: 0; z-index: 60;
  display: none;
  background: rgba(0, 16, 38, 0.35); /* dims the still-visible page half */
}
body.menu-open .menu-overlay { display: block; }
@keyframes menu-panel-in {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}
.menu-overlay__panel {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: min(654px, 100%);
  background: var(--c-navy);
  color: var(--c-white);
  display: flex; flex-direction: column;
  overflow: hidden;
  animation: menu-panel-in 0.45s cubic-bezier(0.25, 1, 0.5, 1) both;
}
/* Their panel carries big translucent diagonal shapes, same motif as the
   welcome accents. Two skewed layers, content stays above them. */
.menu-overlay__panel::before,
.menu-overlay__panel::after {
  content: ""; position: absolute; pointer-events: none;
  background: rgba(255, 255, 255, 0.05);
}
.menu-overlay__panel::before {
  top: -10%; left: -30%; width: 75%; height: 55%;
  transform: skew(-24deg) rotate(-18deg);
}
.menu-overlay__panel::after {
  bottom: 6%; right: -25%; width: 70%; height: 50%;
  transform: skew(-24deg) rotate(-18deg);
  background: rgba(255, 255, 255, 0.04);
}
.menu-overlay__close {
  position: absolute; top: 32px; right: 40px; z-index: 3;
  width: 46px; height: 46px; border: 0;
  border-radius: 50%; background: var(--c-white); color: var(--c-secondary);
  font-size: 1.5rem; line-height: 1; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform 0.3s ease;
}
.menu-overlay__close:hover, .menu-overlay__close:focus-visible { transform: rotate(90deg); }
.menu-overlay__inner {
  position: relative; z-index: 2;
  flex: 1; overflow-y: auto;
  display: flex; align-items: flex-start; justify-content: flex-end;
  padding: 96px 200px 40px 48px;
}
/* Top-level column right-aligned against the divider; children fly out in
   the 225px column right of it. */
.primary-nav > ul {
  list-style: none; margin: 0; padding: 0 24px 0 0;
  border-right: 1px solid rgba(255,255,255,0.35);
  text-align: right;
  max-width: 360px;
}
.primary-nav > ul > li { position: static; }
.primary-nav > ul > li > a {
  display: block; padding: 5px 0;
  font-family: var(--font-display); font-size: clamp(1.7rem, 2.9vw, 38px);
  line-height: 1.29;
  text-transform: uppercase; letter-spacing: 0.02em;
  color: var(--c-white); text-decoration: none;
  transition: color 0.3s ease;
}
.primary-nav > ul > li > a:hover,
.primary-nav > ul > li > a:focus-visible { color: var(--c-accent); }
.primary-nav .sub-menu {
  list-style: none; margin: 0; padding: 0;
  position: absolute; top: 120px; right: 24px; width: 176px;
  text-align: left;
  opacity: 0; visibility: hidden; transform: translateX(8px);
  transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s;
}
.primary-nav > ul > li:hover > .sub-menu,
.primary-nav > ul > li:focus-within > .sub-menu {
  opacity: 1; visibility: visible; transform: translateX(0);
}
.primary-nav .sub-menu a {
  display: block; padding: 5px 0; color: var(--c-white);
  text-decoration: none; font-size: 14px; line-height: 21px;
  transition: color 0.3s ease;
}
.primary-nav .sub-menu a:hover, .primary-nav .sub-menu a:focus-visible { color: var(--c-accent); }
/* Bottom action bar: two icon CTAs on the secondary color, thin divider. */
.menu-overlay__actions {
  position: relative; z-index: 2;
  flex: 0 0 auto;
  display: flex; align-items: stretch;
  background: var(--c-secondary); color: var(--c-white);
  min-height: 88px;
}
.menu-overlay__actions a {
  flex: 1;
  display: inline-flex; align-items: center; justify-content: center; gap: 12px;
  color: var(--c-white); text-decoration: none;
  font-size: 14px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  transition: background-color 0.3s ease;
}
.menu-overlay__actions a + a { border-left: 1px solid rgba(255,255,255,0.35); }
.menu-overlay__actions a:hover, .menu-overlay__actions a:focus-visible { background: rgba(0,0,0,0.14); }
.menu-overlay__actions svg { flex: 0 0 auto; }
@media (max-width: 900px) {
  .menu-overlay__panel { width: 100%; }
  .menu-overlay__inner { justify-content: center; padding: 88px 24px 32px; }
  .primary-nav > ul { border-right: 0; text-align: center; padding-right: 0; }
  /* No hover on touch: children render inline under their parent. */
  .primary-nav .sub-menu {
    position: static; width: auto; opacity: 1; visibility: visible;
    transform: none; text-align: center; padding: 2px 0 10px;
  }
}

/* Search -- compact dropdown panel over the header's right side, matched
   to bdcs.org 2026-08-27: a 740x94 navy bar at the very top, white 16px
   text over a thin underline, "search here.." placeholder, accent
   magnifier at the right. Not a full-screen takeover. */
.search-overlay {
  position: fixed; inset: 0; z-index: 70;
  display: none; background: transparent;
}
body.search-open .search-overlay { display: block; }
@keyframes search-panel-in {
  from { transform: translateY(-100%); }
  to   { transform: translateY(0); }
}
.search-panel {
  position: absolute; top: 0; right: 0;
  width: min(740px, 100%); min-height: 94px;
  background: var(--c-navy); color: var(--c-white);
  display: flex; align-items: center;
  padding: 24px 28px 24px 42px;
  border-radius: 0 0 0 6px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
  animation: search-panel-in 0.35s cubic-bezier(0.25, 1, 0.5, 1) both;
}
.search-panel form { flex: 1; }
.search-panel__field {
  display: flex; align-items: center; gap: 12px;
  border-bottom: 1px solid rgba(255,255,255,0.75);
  padding-bottom: 8px;
}
.search-panel input[type="search"] {
  flex: 1; border: 0; background: transparent; color: var(--c-white);
  font-family: var(--font-body); font-size: 16px; outline: none;
}
.search-panel input[type="search"]::placeholder { color: rgba(255,255,255,0.85); }
.search-panel input[type="search"]:focus-visible { box-shadow: none; }
.search-panel button[type="submit"] {
  border: 0; background: transparent; color: var(--c-accent);
  cursor: pointer; display: inline-flex; padding: 2px;
}
.search-overlay__close {
  margin-left: 18px;
  width: 36px; height: 36px; border: 0; flex: 0 0 auto;
  border-radius: 50%; background: transparent; color: rgba(255,255,255,0.8);
  font-size: 1.3rem; cursor: pointer;
}
.search-overlay__close:hover, .search-overlay__close:focus-visible { color: var(--c-white); }

/* ==========================================================================
   Hero
   ========================================================================== */
/* `100vh - --header-h`, not `100vh`: `#main` now offsets the whole page by
   the header height, so a full-viewport hero on top of that offset would
   push the first screen a bar's worth past the fold and take the slider
   controls -- which sit 35px off the hero's bottom edge -- with it. The
   subtraction keeps header plus hero at exactly one screen. */
.hero {
  position: relative;
  /* `isolation: isolate` is LOAD-BEARING, not tidiness -- same fix, and the
     same reason, as `.values__stage` further down. The slide stack below
     only ever needs to order itself; contained here, it cannot reach the
     rest of the page at all. Before this, the slides sat at 998/999/1000 in
     the ROOT stacking context and painted straight over the fixed header
     (z-index 50), the menu overlay (60) and the search overlay (70). That
     was invisible while the header was transparent over the hero and
     surfaced the moment it became solid on 2026-08-27: Brian, "there is now
     something wrong with the hamburger menu, it seems to go behind the
     slider."

     The fix is containment, NOT a bigger number on the header. Raising the
     header to 1100 and the overlays higher still is the same arms race that
     produced 998/999/1000 in the first place, and the next person loses it
     again. */
  isolation: isolate;
  min-height: calc(100vh - var(--header-h));
  min-height: calc(100svh - var(--header-h));
  display: flex; align-items: flex-end;
  background: var(--c-navy);
  overflow: hidden;
}
/* The hero cross-fade, matched to bdcs.org 2026-08-26 by sampling their
   live slideshow through a full slide change. Theirs is Slick in fade
   mode over 0.5s -- the SAME curve as their core-values module, measured
   independently: 0.122 / 0.332 / 0.545 / 0.706 / 0.818 / 0.896 / 0.948 /
   0.980 at 50ms intervals, settling at ~500ms. Ours was 0.9s, which read
   noticeably slower than theirs.

   z-index mirrors Slick's ORDER -- resting slide under active under
   entering -- but at 1/2/3 rather than Slick's literal 998/999/1000. The
   three-figure numbers were copied off bdcs.org and were never needed:
   these slides only rank against each other, and `.hero` isolates them
   (see above), so the scale inside is free. Same mechanism as
   `.values__slide`; see the switcher in main.js. */
.hero__slide {
  position: absolute; inset: 0;
  opacity: 0;
  z-index: 1;
  transition: opacity 0.5s ease;
  background-size: cover; background-position: center;
  display: flex; align-items: flex-end;
}
.hero__slide.is-active { opacity: 1; z-index: 2; }
.hero__slide.is-entering { z-index: 3; }
.hero__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
.hero__slide::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(to top, rgba(0,16,38,0.82) 0%, rgba(0,16,38,0.32) 32%, rgba(0,16,38,0) 62%),
    linear-gradient(to left, rgba(0,16,38,0.55) 0%, rgba(0,16,38,0) 38%);
}
.hero__content {
  position: relative; z-index: 2;
  width: 100%;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 24px;
  padding: 0 40px 48px;
}
.hero__words { margin-left: auto; text-align: right; }
/* Measured on bdcs.org's live hero caption at a 1324px viewport:
   h4 eyebrow 31px/59px Poppins 600 uppercase white with letter-spacing
   NORMAL (ours carried 0.28em, which spread the word noticeably wider
   than theirs), and h2 114px/87px Anton -- a line-height of 0.763, much
   tighter than the 0.95 ours used. Their markup is `<h2><em>word</em></h2>`
   where the em is #FFBC19; ours already colors the big word with the
   accent token, so that part already matched. */
.hero__eyebrow {
  display: block;
  color: var(--c-white); font-weight: 600;
  font-size: clamp(1rem, 2.34vw, 31px);
  line-height: 1.9;
  letter-spacing: normal; text-transform: uppercase;
}
.hero__word {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(3.4rem, 8.61vw, 114px);
  line-height: 0.763;
  color: var(--c-accent);
  text-transform: uppercase;
  text-shadow: 0 4px 24px rgba(0,0,0,0.35);
}

/* --------------------------------------------------------------------------
   Hero opening -- the caption entrance.

   Brian, 2026-08-27 (third report of the same miss): "still do not have
   the nice site opening like we had on the original template and the
   original site." Diagnosis on the live demo confirmed the theme NEVER
   had one -- the hero is deliberately excluded from the scroll-reveal
   targets (it is already in view at load), so its words simply existed at
   full opacity in the first painted frame (rAF sampling: o=1, tr=none
   from frame one).

   This is a CSS ANIMATION keyed off `.is-active`, not the observer
   machinery, for three reasons:
   1. The observer-same-frame trap: an element already in view at load can
      be revealed in the same frame it first paints, so a transition from
      a hidden base state never renders visibly. An animation with
      `fill-mode: both` starts from its own first keyframe at first paint,
      guaranteed, JS or no JS.
   2. The base state stays VISIBLE (the theme's own hard rule: no content
      is permanently hidden if anything fails) -- keyframes carry the
      hidden start, so with CSS off or animations unsupported the words
      are simply there.
   3. Because the selector is `.is-active`, the SAME entrance replays on
      every slide change (the class is re-added), which is what the
      reference slideshow does -- the caption rises on each new slide,
      composing with the 0.5s cross-fade above.

   Timing: eyebrow rises first (0.7s, 0.12s delay), the big word follows
   (0.85s, 0.24s delay), controls fade up once per page load (0.5s, 0.8s
   delay). Ease-out quart -- fast start, long settle, the same feel as the
   section entrances. Reduced-motion visitors get the global 0.01ms
   animation-duration override, i.e. the finished frame immediately. */
@keyframes hero-caption-in {
  from { opacity: 0; transform: translateY(44px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes hero-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.hero__slide.is-active .hero__eyebrow {
  animation: hero-caption-in 0.7s cubic-bezier(0.25, 1, 0.5, 1) 0.12s both;
}
.hero__slide.is-active .hero__word {
  animation: hero-caption-in 0.85s cubic-bezier(0.25, 1, 0.5, 1) 0.24s both;
}
.hero__controls {
  animation: hero-fade-in 0.5s ease 0.8s both;
}
/* Hero controls, rebuilt 2026-08-26 to bdcs.org's measured geometry.
   Theirs is `.holder-controls`, `position: absolute; left: 0; bottom: 35px`
   laid out as a horizontal flex row FLUSH TO THE LEFT EDGE -- not the
   vertical stack inset 24px that this theme had. Measured positions in a
   1324x786 hero: prev 53x53 at x=0, next 53x53 at x=54 (a 1px gap), pause
   28x28 at x=123 (16px after the arrows) and y=711, which is vertically
   centred on the 53px arrow row. Their arrows are SQUARE (border-radius 0)
   and filled with their secondary red; ours read as circles before this.

   `--c-secondary` falls back to the theme primary, so Cristo Rey's own
   site keeps its blue and only the bdcs demo goes red. */
.hero__controls {
  /* MUST clear the slides. The controls are the slides' siblings inside
     `.hero`, so they have to out-rank the entering slide explicitly --
     that is still true, only the scale changed: the slides are 1/2/3 now
     (see .hero__slide) so 4 is the number that clears them. bdcs.org does
     not hit this because their controls live outside the slick list. */
  position: absolute; left: 0; bottom: 35px; z-index: 4;
  display: flex; flex-direction: row; align-items: center; gap: 0;
}
.hero__controls .arrow-btn {
  width: 53px; height: 53px;
  border-radius: 0;
  background: var(--c-secondary);
}
.hero__controls .arrow-btn + .arrow-btn { margin-left: 1px; }
.hero__controls .arrow-btn:hover,
.hero__controls .arrow-btn:focus { background: var(--c-primary-dark); }
.hero__pause {
  width: 28px; height: 28px; margin-left: 16px;
  border: 1px solid rgba(255,255,255,0.6); border-radius: 50%;
  background: transparent; color: var(--c-white); cursor: pointer;
  font-size: 0.6rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}

/* ==========================================================================
   Welcome + program tiles
   ========================================================================== */
/* overflow-x only (not overflow: hidden) so the corner accent and soft
   panel below can bleed past the section's top/bottom edges the way
   bdcs.org's do, while still guarding against a horizontal scrollbar from
   the accent's right-edge bleed. */
.welcome { padding: 88px 0 40px; position: relative; overflow-x: clip; }
/* Soft panel sitting behind the lower portion of the intro copy — the
   decorative layer bdcs.org's welcome section has and ours was missing.
   Kept as a plain rectangle in our own bg-light token rather than copying
   their asset.
   2026-08-26: switched from `height: calc(100% - 160px)` (a percentage
   against an auto-height, position:relative parent -- fragile, and the
   thing that let this panel's bottom edge wander outside `.welcome`) to a
   `top` + `bottom` inset pair. Both edges are now pinned directly to
   `.welcome`'s own box with no percentage math, so the panel physically
   cannot extend past its parent's top or bottom -- which is the actual
   fix for the hard clip line Brian kept seeing, not a clipping property
   on `.welcome`, `.hero` or `.tiles`.
   2026-08-26 (same day, second pass): that fix traded a clipping bug for
   a new visible seam -- `top: 96px` / `bottom: 24px` left a band of plain
   white between the panel's edges and `.welcome`'s actual top/bottom, and
   the white -> gray -> white transition reads as a hard line even with no
   literal `border` anywhere. Panel is now flush with `.welcome`'s own box
   (`top: 0; bottom: 0`) so it spans the section edge-to-edge with no gap.
   Confirmed safe against the clipping bug this replaces: `.welcome` still
   has `overflow-x: clip` only (no vertical overflow control), and this
   panel never extends past `top: 0` / `bottom: 0` of its own parent, so
   there is nothing here that can bleed into `.hero` above or `.tiles`
   below the way the old negative-offset accent did. */
.welcome::before {
  content: "";
  position: absolute;
  left: 0;
  right: 15px;
  top: 0;
  bottom: 0;
  background: var(--c-bg-light);
  z-index: 0;
}
/* Oversized rotated line-art accent in the corner, on-brand (gold hairlines)
   instead of their SVG asset — a low-key background layer, not a focal
   graphic.
   2026-08-26: previously `top: -70px; right: -70px`, which pushed this
   element's box above `.welcome`'s own top edge -- physically into the
   space occupied by the `.hero` section immediately above it (they are
   adjacent in flow with no gap). `.hero` has `overflow: hidden` for its
   own slider, and a later sibling painting into an earlier sibling's box
   still gets cut off wherever that earlier element's own stacking/overflow
   rules apply, which is what read as a hard clipped edge at the top.
   Keeping `top: 0` (and a same-section `right: -40px`, safe because
   `.welcome` itself clips the X axis on purpose) means the whole accent
   now lives entirely inside `.welcome`'s own painted area -- still an
   oversized corner bleed off the right edge, never crossing into the
   hero or being painted over by `.tiles` below. */
.welcome::after {
  content: "";
  position: absolute;
  top: 0;
  right: -40px;
  width: 300px;
  height: 300px;
  z-index: 1;
  background-image: repeating-linear-gradient(45deg, var(--c-accent) 0 2px, transparent 2px 24px);
  opacity: 0.3;
  transform: rotate(12deg);
  pointer-events: none;
}
.welcome__head { position: relative; z-index: 2; max-width: 920px; }
/* 2026-08-26: bdcs.org's live welcome heading measures a fixed 120px at a
   1400px viewport (verified directly with getComputedStyle after
   scrolling it fully into view and off, twice independently landing on
   120px -- see brain). The previous clamp topped out at 4.6rem (73.6px),
   visibly smaller than the real site at every width Brian screenshotted.
   Sized generously here to read the same bold scale; "Cristo Rey San
   Diego" is longer than "Bishop Dunne" so the block above got more room
   (max-width 760px -> 920px) rather than shrinking the type to fit one
   line -- wrapping to two lines at this size is normal for this kind of
   display heading and matches how bdcs.org itself wraps longer titles
   elsewhere on the page. */
/* bdcs.org's welcome h2 measures 120px/87px Anton -- line-height 0.725,
   40px bottom margin (measured 2026-08-26). 9vw hits 119.2px at their
   1324px reference viewport and the 7.5rem ceiling caps it at 120px. */
.welcome__head .display-heading { font-size: clamp(3rem, 9vw, 7.5rem); line-height: 0.725; margin-bottom: 40px; }
/* The shared .eyebrow (0.95rem) reads noticeably smaller than bdcs.org's
   welcome eyebrow. Re-measured 2026-08-26 on a fully-settled element
   (opacity 1, transform none, animation finished) at 25px/31px/700/
   uppercase -- corrects an earlier 21px reading taken before the reveal
   animation had finished, with a -10px pull into the heading below.
   Scoped to this section only — the other front page sections were not
   flagged and keep the shared size. */
/* bdcs.org: 25px/31px Poppins 700, `margin: 0 0 20px 5px`. The -10px this
   carried was tuned to the old 0.94 heading line-height; once the heading
   went to their real 0.725 the first line box rode up and covered the
   eyebrow completely (caught in a screenshot, not in any computed value). */
.welcome__head .eyebrow { font-size: 25px; line-height: 31px; font-weight: 700; margin: 0 0 20px 5px; }
/* 19px/37px/500 is bdcs.org's REAL desktop value, re-measured 2026-08-27
   at a 1920 viewport. The earlier 15px/26px reading was a narrow-viewport
   value (taken at 1324) — Brian asked three times for this paragraph
   bigger and he was right. His eye is the spec (decision via Otto,
   2026-08-27); do not "correct" this back to 15px. Weight/color
   unchanged. */
.welcome__head .section-intro p { font-size: 19px; line-height: 37px; font-weight: 500; color: var(--c-gray); }
/* CTA + trailing divider line, matching bdcs.org's bordered "Learn More"
   box (145x61, uppercase, hover fills solid) plus the thin rule that runs
   from it — reusing this theme's own button styling and color tokens
   rather than any bdcs asset. */
.welcome__cta { display: flex; align-items: center; gap: 24px; margin-top: 24px; }
.welcome__cta-line { flex: 0 1 133px; height: 1px; background: var(--c-line); }
.btn--box {
  width: 145px; height: 61px; padding: 0;
  /* Measured on their live button: 14px/28px Poppins 700. Ours inherited
     13.6px/23.8px at weight 600 from `.btn`. */
  font-size: 14px; line-height: 28px; font-weight: 700;
  display: flex; align-items: center; justify-content: center; text-align: center;
  border-radius: 3px; border-width: 1px;
  /* 2026-08-26: this button was inheriting `.btn`'s default border/text
     color (--c-primary, this theme's blue), which is why it rendered
     navy instead of the red bdcs.org's real "learn more" button uses
     (measured directly: getComputedStyle -> rgb(196, 2, 51) / #C40233,
     their --secondary-color). Scoped to --c-secondary so only THIS
     button can differ from the theme's primary color; --c-secondary
     defaults to --c-primary (functions.php), so Cristo Rey's real theme
     is unchanged unless a secondary brand color is explicitly set in the
     Customizer. The bdcs demo profile sets it to bdcs.org's real
     #C40233 (dev-preview/render.php) since that demo exists purely to
     prove parity with the live site. */
  border-color: var(--c-secondary);
  color: var(--c-secondary);
}
.btn--box:hover, .btn--box:focus {
  background: var(--c-secondary);
  border-color: var(--c-secondary);
  color: var(--c-white);
}

/* `overflow-x: clip` added 2026-08-26 with the right-to-left track
   entrance further down this file: the track is parked at
   translateX(160px) until the section reveals, and without this the
   section would hand the document a horizontal scrollbar for the
   duration. `clip` rather than `hidden` because it does not create a
   scroll container of its own and legally pairs with `overflow-y:
   visible`, so the odd-tile 20px offset and the drop shadows are
   unaffected. */
.tiles { position: relative; padding: 40px 0 88px; overflow-x: clip; }
/* Track geometry measured on bdcs.org 2026-08-26. Theirs is NOT full-bleed
   with even padding: the tile strip is inset 215px on the LEFT (20px module
   padding + 195px inner padding) and runs FLUSH to the right edge, which is
   what creates the gutter their carousel arrows sit in. Tile pitch is 380px
   on a 364px tile, so the gap is 16px, not the 24px this theme used. */
.tiles__track {
  display: flex; gap: 16px;
  align-items: flex-start;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding: 0 0 0 215px;
  /* Bottom padding carries the 20px stagger the odd tiles get at >=800px
     (see .tile:nth-child(even)) plus the drop shadow, so neither is cut off
     by the track's own scroll box. */
  padding: 8px 0 44px 215px;
  scrollbar-width: none;
}
.tiles__track::-webkit-scrollbar { display: none; }

/* --------------------------------------------------------------------------
   Program tiles — the hover choreography, rebuilt 2026-08-26 to match the
   real mechanics of bdcs.org's tile module rather than approximate them.

   Measured directly off the live site (computed styles + their stylesheet,
   see the tech-director brain for the capture method). Their resting tile
   shows a photo, a dark bottom scrim, a small colored corner wedge and the
   program name. On :hover / :focus-within SIX things fire at once:

     1. the photo scales to 1.2                              (0.5s)
     2. the corner wedge grows to calc(100% + 450px) square
        at 0.86 opacity, flooding the whole tile in brand
        color -- this is the signature move                  (0.7s)
     3. the resting label slides out to the left, -200%       (0.5s)
     4. a detail panel slides in from off-screen right        (0.5s)
     5. the scrim lifts above the wash so the copy stays
        readable
     6. the round arrow button (link tiles only) becomes
        reachable, and scales to 1.1 on its own hover

   Two deliberate differences from bdcs.org, both noted where they occur:
   the panel's resting offset is calc(100% + 30px) instead of their fixed
   340px (their number leaves a sliver of panel visible on a 364px tile),
   and long program names wrap instead of clipping. Colors come from this
   theme's own tokens throughout -- none of Bishop Dunne's values are
   hardcoded here.
   -------------------------------------------------------------------- */
.tile {
  position: relative;
  display: block;
  flex: 0 0 285px;
  height: 433px;
  scroll-snap-align: start;
  overflow: hidden;
  text-decoration: none;
  background: var(--c-navy);
}
.tile img, .tile__img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  object-position: center 25%;
  z-index: 0;
  transition: transform 0.5s ease;
}
.tile:hover img, .tile:focus-visible img, .tile:focus-within img,
.tile:hover .tile__img, .tile:focus-visible .tile__img, .tile:focus-within .tile__img { transform: scale(1.2); }

/* Bottom scrim. bdcs.org's is a fixed 274px band of black at 0.86, not a
   percentage — keeping their geometry so the label sits in the same
   density of shadow at both tile sizes. */
.tile::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  height: 274px;
  opacity: 0.86;
  background: linear-gradient(180deg, rgba(0,0,0,0) 0%, #000 90.41%);
  transition: opacity 0.3s ease;
}

/* The corner wedge, and the wash it becomes. The clip-path is a right
   triangle anchored at the box's bottom-left corner, so as the box grows
   from 105x75 to calc(100% + 450px) square the hypotenuse sweeps up and
   to the right and ends up covering the tile completely. Growing the box
   is what animates — the clip-path itself never changes. */
.tile::after {
  content: ""; position: absolute; left: 0; bottom: 25px; z-index: 2;
  width: 105px; height: 75px;
  background: var(--c-primary);
  clip-path: polygon(0 100%, 100% 100%, 0 0);
  transition: width 0.7s ease, height 0.7s ease, opacity 0.7s ease;
}
.tile:nth-child(even)::after { background: var(--c-tile-alt); }
.tile:hover::after, .tile:focus-visible::after, .tile:focus-within::after {
  opacity: 0.86;
  width: calc(100% + 450px);
  height: calc(100% + 450px);
}
/* Once the wash is up, the scrim moves above it so the panel copy keeps a
   dark base to sit on, exactly as bdcs.org reshuffles its two layers. */
.tile:hover::before, .tile:focus-visible::before, .tile:focus-within::before { z-index: 3; }

/* Resting label. bdcs.org pins theirs at left 27px / bottom 0 with a 38px
   Anton face on an 87px line box; the padding below reproduces that same
   baseline while letting a long program name ("Corporate Work Study")
   wrap instead of being clipped by the tile's overflow. */
.tile__label {
  position: absolute; left: 27px; right: 20px; bottom: 0; z-index: 4;
  padding-bottom: 24px;
  font-family: var(--font-display);
  font-size: 38px; line-height: 1.05;
  text-transform: uppercase; letter-spacing: 0.02em;
  color: var(--c-white);
  text-shadow: 0 2px 10px rgba(0,0,0,0.45);
  transition: transform 0.5s ease;
}
.tile--has-panel:hover .tile__label,
.tile--has-panel:focus-visible .tile__label,
.tile--has-panel:focus-within .tile__label { transform: translateX(-200%); }

/* The detail panel, parked off the right edge. */
.tile__panel {
  position: absolute; left: 0; right: 0; bottom: 30px; z-index: 4;
  display: block;
  padding: 0 15px;
  text-align: center;
  color: var(--c-white);
  transform: translateX(calc(100% + 30px));
  transition: transform 0.5s ease;
}
.tile--has-panel:hover .tile__panel,
.tile--has-panel:focus-visible .tile__panel,
.tile--has-panel:focus-within .tile__panel { transform: translateX(0); }
.tile__title {
  display: block;
  font-family: var(--font-display);
  font-size: 38px; line-height: 1;
  text-transform: uppercase; letter-spacing: 0.02em;
  margin-bottom: 10px;
}
.tile__desc {
  display: block;
  font-family: var(--font-body);
  font-size: 17px; line-height: 1.4; font-weight: 500;
}
.tile__arrow {
  display: block;
  width: 43px; height: 43px;
  margin: 18px auto 0;
  border-radius: 50%;
  background: var(--c-white);
  position: relative;
  transition: transform 0.5s ease;
}
/* Chevron drawn from borders rather than an icon font, so the theme picks
   up no third-party asset for it. */
.tile__arrow::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 9px; height: 9px;
  border-top: 2px solid var(--c-primary);
  border-right: 2px solid var(--c-primary);
  transform: translate(-65%, -50%) rotate(45deg);
}
.tile:hover .tile__arrow, .tile:focus-visible .tile__arrow { transform: scale(1.1); }

/* Touch devices have no hover, and bdcs.org has no fallback for them —
   verified live at 375px with touch emulation on 2026-08-26: their panel
   stays parked at translateX(340px) and the description is unreachable.
   Rather than copy that, tiles rest in the revealed state wherever the
   pointer cannot hover, so every visitor can read the same content. This
   is the one deliberate behavioral divergence from the reference site. */
@media (hover: none) {
  .tile--has-panel::after { opacity: 0.86; width: calc(100% + 450px); height: calc(100% + 450px); }
  .tile--has-panel::before { z-index: 3; }
  .tile--has-panel .tile__label { transform: translateX(-200%); }
  .tile--has-panel .tile__panel { transform: translateX(0); }
}

@media (min-width: 800px) {
  .tile { flex-basis: 364px; height: 550px; }
  /* bdcs.org staggers every odd tile 20px down the page at this width.
     They do it with translateY because their tiles are carousel slides.
     Here it stays a margin. Margin cannot collide with anything and
     produces the identical offset -- the track sets `align-items:
     flex-start` and carries the matching bottom padding for it. (Until
     2026-08-26 a transform here would also have been silently discarded,
     because the scroll-reveal system owned `transform` on `.tile`. That
     reveal now moves `.tiles__track` as one block instead, so `.tile`'s
     transform is free -- but there is still no reason to reach for it.) */
  /* PHASE MATTERS and ours was inverted. On bdcs.org the FIRST visible tile
     sits high and the SECOND drops 20px (measured: middle school top=33,
     high school top=13, collegiate 33, future ready 13). Ours dropped the
     first tile instead, so the whole row zig-zagged the opposite way. They
     do it with translateY; margin is used here because it cannot collide
     with anything, and produces the identical offset. */
  .tile:nth-child(even) { margin-top: 20px; }
  .tile__desc { font-size: 19px; line-height: 31px; }
}
/* Measured on bdcs.org: `.controls-wrapper` is 55px wide, sits 120px from
   the module's left edge -- inside the 215px track gutter, not hard against
   the viewport edge as this theme had it -- and stacks two 55x55 CIRCULAR
   red arrows with a 5px gap. (Their hero arrows are square; their tile
   arrows are round. Both are reproduced as measured.) */
.tiles__nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 5px; left: 120px; z-index: 4;
}
/* Their tile arrows are filled with the secondary color, not the primary --
   measured rgb(196, 2, 51). `.arrow-btn`'s default is --c-primary, which
   rendered these navy against their red. --c-secondary falls back to the
   primary, so Cristo Rey's own site is unchanged. */
.tiles__nav .arrow-btn { width: 55px; height: 55px; background: var(--c-secondary); }
.tiles__nav .arrow-btn:hover,
.tiles__nav .arrow-btn:focus { background: var(--c-primary-dark); }

/* ==========================================================================
   Values -- rebuilt 2026-08-26 to match bdcs.org's "image-switch" module.

   EVERY NUMBER BELOW WAS MEASURED on the live site at a 1324px viewport
   (getComputedStyle + getBoundingClientRect), not estimated. Their module
   is `.fsElement.fsContainer.image-switch`, a Slick slider in `fade: true`
   mode. What it actually is, as opposed to what it looks like:

     - The section is 756px tall and has NO background of its own and no
       overlay tint. Each slide carries a full-bleed photo (object-fit:
       cover) and the photo IS the background.
     - Six slides for five values: slide 0 is an intro card, slides 1-5 are
       the values. Their dots list has six items and hides the first with
       `display: none`, which is why NO value name is highlighted while the
       intro slide is showing.
     - The white card is 500px wide, absolutely positioned at the BOTTOM
       LEFT, and deliberately hangs 40px BELOW the section's bottom edge
       (`bottom: -40px`). That overhang is a design feature, not a bug.
     - The track never translates. Slides cross-fade in place over 0.5s
       ease. See the switcher in assets/js/main.js.

   Two corrections to numbers that were circulating before this measurement,
   recorded so nobody reinstates them: the slide photo is NOT 441px tall (it
   fills the slide), and the card is NOT `calc(100% - 40px)` centered (that
   is their narrow-viewport rule). Their prev/next arrows compute to 0x0 at
   desktop -- they are a narrow-viewport control only, and are reproduced
   that way here.

   `overflow-x: clip` is load-bearing, not tidiness: the entrance animation
   parks the stage and the value list off to the RIGHT of the viewport
   before they slide in, which on a 375px phone put 136px of the page
   outside the viewport and gave the whole document a horizontal scrollbar.
   `clip` rather than `hidden` because it contains the horizontal overhang
   without turning the section into a scroll container, and it is the one
   overflow value that still pairs with `overflow-y: visible` -- which the
   card's 40px bottom overhang depends on.
   ========================================================================== */
.values {
  position: relative;
  /* Color only -- NO background-image. Each slide carries its own photo in
     `.values__photo`; a section-level image would sit under the whole stack
     and stay visible behind and through every cross-fade (fixed 2026-08-27,
     see the note at the top of template-parts/values.php). The color stays:
     it is the backstop before photos load and behind anything a fade lets
     through, and without it a mid-fade frame can flash white. */
  background-color: var(--c-navy);
  overflow-x: clip;
  /* bdcs.org's module carries exactly this padding, so their photo spans
     viewport-10 to viewport-10 rather than literally edge to edge. The
     stage is a DIRECT child of this section -- no `.container` -- which is
     what makes the photo full-bleed. See the note in values.php. */
  padding: 0 10px;
  /* Their module carries margin-bottom: 60px, and the card hangs 40px into
     it. Margin rather than padding so the card overhangs onto the page
     background exactly as theirs does. */
  margin-bottom: 60px;
}
/* `.values` is the only positioned ancestor in this section, on purpose. The
   value list is `position: absolute; right: 10px` and resolves against it, so
   its rules reach the viewport edge the way bdcs.org's do. The `.container`
   and `.values__inner` wrappers were REMOVED from the template entirely on
   2026-08-26 -- while they existed, the photo was trapped in the site's
   max-width and stopped ~66px short of each edge. Do not reintroduce them. */

/* The slider viewport. Their `.slick-list` is 756px tall with
   `overflow: visible`, which is what lets the card hang out of the bottom. */
/* z-index 1 + `isolation: isolate` is LOAD-BEARING, not tidiness.
   REGRESSION FIXED 2026-08-27, and worth understanding because it was
   invisible until it wasn't. The slides carry z-index 998/999/1000 (they
   mirror Slick's own). Those only ever stayed BELOW `.values__list`
   (z-index 3) by accident: the old right-to-left entrance put a
   `transform` on `.values__stage`, which silently made it a stacking
   context and trapped the 999s inside it. Moving the entrance onto
   `.values` itself removed that transform -- and the slides' 999
   escaped into the section's stacking context, painted straight over the
   value names, and Brian's list vanished behind the photo. Nothing about
   the list changed; its containing block was never the problem.

   `isolation: isolate` makes the stacking context explicit and
   unconditional, so it cannot be lost again by moving a transform
   around. Order in this section is now stated rather than emergent:
   stage 1, scrim 2, list 3. */
.values__stage { position: relative; z-index: 1; isolation: isolate; }
.values__slides {
  position: relative;
  height: 756px;
  overflow: visible;
}

/* --------------------------------------------------------------------
   The cross-fade.

   NO-JS IS THE BASE STATE, not a fallback bolted on afterwards. Without
   `html.has-js` the slides are ordinary stacked blocks and every value's
   statement is readable by scrolling -- strictly better than bdcs.org,
   whose statements are unreachable with JS off. The absolute stacking and
   the fade only switch on once JS is present.

   With JS: every slide is stacked in the same box at opacity 0 and the
   active one is at opacity 1. Moving `.is-active` from one slide to
   another in a single frame makes the outgoing and incoming slides
   transition SIMULTANEOUSLY -- which is what bdcs.org does (sampled on
   their live site: 108ms 0.878/0.122, 208ms 0.454/0.546, 308ms
   0.181/0.819, settled by ~500ms). z-index mirrors Slick's own: inactive
   998, active 999, entering 1000.
   -------------------------------------------------------------------- */
html.has-js .values__slides > .values__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  z-index: 998;
  transition: opacity 0.5s ease;
}
html.has-js .values__slides > .values__slide.is-active {
  opacity: 1;
  pointer-events: auto;
  z-index: 999;
}
html.has-js .values__slides > .values__slide.is-entering { z-index: 1000; }

/* THE CARD SLIDES, THE PHOTO ONLY FADES -- and the two run at different
   speeds on purpose. Measured off bdcs.org 2026-08-27 by sampling their
   card through a real dot click:

     resting   opacity 0, translateX(-200px)
     active    opacity 1, translateX(0)
     duration  1s   (the photo cross-fade above stays 0.5s)

     t=129ms  op 0.107  x -178.6      t=488ms  op 0.767  x -46.6
     t=251ms  op 0.352  x -129.6      t=608ms  op 0.874  x -25.3
     t=368ms  op 0.604  x  -79.2      t=729ms  op 0.944  x -11.1

   The outgoing card runs the mirror at the same time, receding 0 ->
   -200px as it fades, which is what makes the swap read as a card being
   replaced rather than a picture dissolving. Brian: "the white box
   associated with it should animate in left to right like the original
   site."

   It is a CHILD of the slide, so its opacity multiplies with the slide's
   0.5s fade rather than competing with it -- the same reason the
   entrance animates `.values` and not the slides.

   The -200px offset puts the card's left edge outside the viewport
   mid-travel; `.values` carries `overflow-x: clip` (see the section
   rule), which contains it without turning the band into a scroll
   container and without clipping the card's 40px bottom overhang. */
html.has-js .values__slides > .values__slide .values__panel {
  opacity: 0;
  transform: translateX(-200px);
  transition-property: opacity, transform;
  transition-duration: 1s;
  transition-timing-function: ease;
}
html.has-js .values__slides > .values__slide.is-active .values__panel {
  opacity: 1;
  transform: translateX(0);
}

/* No-JS: slides simply stack down the page and all content is readable. */
html:not(.has-js) .values__slides { height: auto; }
html:not(.has-js) .values__slide { position: relative; margin-bottom: 40px; }

/* No photo anywhere: shorten the band and drop the scrim, so the section
   reads as a deliberate color block rather than a broken slider. This is
   the live Cristo Rey state until the school supplies photos. */
.values--no-photo .values__slides { height: 420px; }
.values--no-photo::after { display: none; }

.values__photo { position: absolute; inset: 0; z-index: 0; }
.values__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
html:not(.has-js) .values__photo { position: relative; height: 480px; }

/* A soft scrim under the value list only.

   DELIBERATE DEVIATION FROM bdcs.org, and the only one in this section.
   They run white value names straight over the photo with no scrim, which
   works on their particular sky-and-skyline image and would fail on a
   photo with a bright right-hand side. Cristo Rey's photo is not chosen
   yet, so a gradient that is invisible over a dark photo and rescues a
   light one is cheap insurance against unreadable navigation. */
.values::after {
  content: "";
  /* z-index 2: above `.values__stage` (1), below `.values__list` (3).
     Until 2026-08-27 this sat at 1 underneath z-999 slides and therefore
     did nothing at all. It only tints from 45% rightward, and the card
     lives at `left: 30px`, so the card stays untouched. */
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(90deg, rgba(0, 25, 52, 0) 45%, rgba(0, 25, 52, 0.45) 100%);
}

/* --------------------------------------------------------------------
   The white card. Measured: 500x336 (intro 381), absolute, left 30px
   inside a container the module pads by 10px, `bottom: -40px`,
   `margin: -50px 0 0`, `padding: 60px 40px`, 1px #ECECEC border.

   `bottom` rather than `top` on purpose: their card has a hard-coded
   height per slide, which only works because a human sized each one. This
   anchors the card to a fixed bottom and lets it grow upward, so the 40px
   overhang survives any length of statement.
   -------------------------------------------------------------------- */
.values__panel {
  position: absolute;
  left: 30px; bottom: -40px; z-index: 40;
  width: min(500px, calc(100% - 60px));
  background: var(--c-white);
  border: 1px solid var(--c-line);
  padding: 60px 40px;
  text-align: left;
}
html:not(.has-js) .values__panel { position: relative; left: 0; bottom: 0; margin: -50px auto 0; }

/* Intro card typography. Their h3 / h2 / h6, measured. */
.values__eyebrow {
  display: block;
  font-family: var(--font-body);
  font-size: 21px; line-height: 21px; font-weight: 700;
  text-transform: uppercase; color: var(--c-gray);
  margin: 0 0 20px; padding: 0 0 5px 5px;
}
.values__display {
  font-family: var(--font-display);
  /* 120px at their 1324px viewport; 9.06vw reproduces it there and scales
     down cleanly on a phone. line-height 110/120 = 0.92. */
  font-size: clamp(3.2rem, 9.06vw, 120px);
  line-height: 0.92; font-weight: 400;
  text-transform: uppercase;
  /* Their red. `--c-secondary` falls back to the theme primary, so the real
     Cristo Rey site renders its own blue and only the bdcs demo goes red. */
  color: var(--c-secondary);
  margin: 0 0 25px;
}
.values__sub {
  font-family: var(--font-body);
  font-size: 19px; line-height: 24px; font-weight: 700;
  color: var(--c-gray); margin: 0 0 15px;
}

/* Value card typography: small lead-in, the word large in brand color,
   then the remainder. Their h6 / h4 / h6. */
.values__lead,
.values__text {
  font-family: var(--font-body);
  font-size: 19px; line-height: 24px; font-weight: 700;
  color: var(--c-gray); margin: 0 0 15px;
}
.values__word {
  font-family: var(--font-body);
  font-size: clamp(1.6rem, 2.72vw, 36px); line-height: 1;
  font-weight: 800; text-transform: uppercase;
  color: var(--c-secondary);
  margin: 0 0 20px;
}
.values__text:last-child { margin-bottom: 0; }

/* --------------------------------------------------------------------
   The value-name list -- their carousel "dots".

   Measured: `ul` absolute at right 0, vertically centered, 283px wide,
   padding-left 20px, text-align right. Each `li` is 69px tall (51px line
   box + 8px padding top and bottom). Titles are 28px/51px Poppins 700
   uppercase, WHITE at rest with a WHITE 32px rule at `right: -5px`; the
   active one is gold with an 80px right padding and a 73px gold rule.
   Transition is `all 0.5s ease`.

   The rule sitting at `right: -5px` of a list pinned to the section's
   right edge is why their rules run to the very edge of the viewport.
   That is reproduced by positioning against `.values`, not `.container`.
   -------------------------------------------------------------------- */
.values__list {
  /* right: 10px reproduces bdcs.org's own geometry -- their module carries
     `padding: 0 10px`, so their list's right edge sits at viewport-10 and
     the rule, at `right: -5px`, ends 5px inside the viewport edge. */
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  z-index: 3;
  list-style: none; margin: 0; padding: 0 0 0 20px;
  width: 283px; max-width: 45%;
  text-align: right;
}
.values__list li { margin: 0; padding: 9px 0; }
.values__list button {
  border: 0; background: transparent; cursor: pointer;
  position: relative; display: inline-block;
  font-family: var(--font-body);
  font-size: clamp(1.05rem, 2.12vw, 28px); line-height: 1.82; font-weight: 700;
  text-transform: uppercase; letter-spacing: normal;
  color: var(--c-white);
  padding: 0 var(--value-pad, 45px) 0 0;
  transition: color 0.5s ease, padding-right 0.5s ease;
}
.values__list button::after {
  content: ""; position: absolute; right: -5px; top: 50%;
  width: var(--value-rule, 32px); height: 1px;
  background: var(--value-rule-color, var(--c-white));
  transform: translateY(-50%);
  transition: width 0.5s ease, background-color 0.5s ease;
}
/* At rest the rule is WHITE and no name is gold -- on the intro slide
   nothing is selected, exactly as on bdcs.org. */
.values__list button:hover,
.values__list button:focus-visible,
.values__list li.is-active button {
  color: var(--c-accent);
  --value-rule: 73px;
  --value-pad: 80px;
  --value-rule-color: var(--c-accent);
}

/* --------------------------------------------------------------------
   Prev / next arrows. Their `.slick-prev` / `.slick-next` compute to 0x0
   at desktop -- present in the DOM, given no size by CSS -- so they are a
   narrow-viewport control on bdcs.org and are reproduced that way. The
   45x45 circle in the theme's secondary color is their own styling.
   -------------------------------------------------------------------- */
.values__arrow {
  display: none;
  width: 45px; height: 45px; border-radius: 50%;
  border: 0; cursor: pointer;
  background: var(--c-secondary);
  position: relative;
}
.values__arrow::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 9px; height: 9px;
  border-top: 2px solid var(--c-white);
  border-right: 2px solid var(--c-white);
}
.values__arrow--prev::before { transform: translate(-35%, -50%) rotate(-135deg); }
.values__arrow--next::before { transform: translate(-65%, -50%) rotate(45deg); }

@media (max-width: 1000px) {
  /* The list cannot stay pinned beside a 500px card at this width, so it
     drops below the stage and the section height goes content-driven. */
  .values__slides { height: 560px; }
  .values__list {
    position: static; transform: none;
    width: auto; max-width: none;
    padding: 0 20px;
    margin-top: 64px;
    display: flex; flex-wrap: wrap; gap: 0 28px;
    justify-content: flex-start;
  }
  .values__list button { font-size: 1.05rem; line-height: 2.2; padding-right: 0; }
  .values__list button::after { display: none; }
  .values::after { background: linear-gradient(180deg, rgba(0, 25, 52, 0) 60%, rgba(0, 25, 52, 0.35) 100%); }
  .values__arrows { display: block; }
  .values__arrow { display: inline-block; position: absolute; top: 180px; z-index: 41; }
  .values__arrow--prev { left: 12px; }
  .values__arrow--next { left: 68px; }
}
@media (max-width: 760px) {
  .values__slides { height: 470px; }
  .values__panel { left: 16px; width: calc(100% - 32px); padding: 36px 26px 40px; }
  .values__eyebrow { font-size: 16px; line-height: 16px; margin-bottom: 14px; }
  .values__sub, .values__lead, .values__text { font-size: 16px; line-height: 22px; }
}

/* ==========================================================================
   Voices (testimonials)
   ========================================================================== */
/* Voices, rebuilt 2026-08-26 against bdcs.org's `posts-main-container`.
   Every number measured live at a 1324px viewport.

   THE TRANSITION IS A SLIDE, NOT A FADE -- the one module on their page
   that moves horizontally. Sampled through a real click: the track
   translates a full slide width (-1324px) over ~1000ms, on the identical
   easing curve their fades use (progress 0.123 / 0.332 / 0.546 / 0.705 /
   0.819 / 0.897 / 0.948 / 0.980 at even fractions -- the same numbers the
   hero and core-values fades produced). So: one easing across the whole
   site, 0.5s for fades and 1s for this slide.

   The old build toggled `display: none` on each voice, which makes a
   slide physically impossible. Now a clipped viewport holds a flex track
   that moves. */
.voices { overflow: hidden; padding: 40px 0 120px; }
.voices__head { text-align: right; }
/* Their eyebrow is 25px/53px Poppins 700 in near-black, not the 15px/26px
   600 grey this theme had; their h2 is 120px/87px Anton (line-height
   0.725) against ours at 88px/88px. */
.voices__head .eyebrow {
  font-size: 25px; line-height: 53px; font-weight: 700;
  color: var(--c-ink); text-transform: uppercase;
}
.voices__head .display-heading { font-size: clamp(3rem, 9vw, 7.5rem); line-height: 0.725; }

/* The viewport is a direct child of `.voices` (not `.container`) so the
   track can clip at the VIEWPORT edge -- padding on the viewport would
   clip at the padding box and let the next slide peek through on the
   right. But the slide CONTENT is not edge-to-edge: re-measured live
   2026-08-27 after Brian's side-by-side ("image is not right up against
   the left. the text doesn't bleed to the edge") -- their portrait sits at
   x=92 in a 1324 viewport (their ~1180 container + 20px) with the
   name/quote column 585px wide ending at the container's right edge. The
   earlier "8px from the viewport edge" note was a mis-measure. So each
   slide carries the CONTAINER inset itself: the same margin+padding math
   as `.container`, on the slide, where it cannot break the clipping. */
.voices__viewport { overflow: hidden; }
.voices__track {
  display: flex;
  transition: transform 1s ease;
  will-change: transform;
}
.voice {
  flex: 0 0 100%;
  display: flex; gap: 56px; align-items: flex-start;
  padding: 40px calc(max((100% - var(--container)) / 2, 0px) + 24px) 0;
}
/* Their portrait is 510x683 and cropped, not 380px wide and free-height. */
.voice__photo { flex: 0 0 510px; max-width: 100%; }
.voice__photo img { width: 100%; height: 683px; object-fit: cover; display: block; }
/* Their quote column measures 585px at 1324; the cap keeps it a readable
   measure on wider screens instead of bleeding toward the edge. */
.voice__body { flex: 1; min-width: 280px; max-width: 640px; }
/* CORRECTED 2026-08-27, and this reverses an earlier change. A previous
   pass set this to Poppins 32px/36px 700 in grey "to match bdcs.org". That
   is the styling of their `.fsBody h3`, which computes `display: none` --
   a hidden fallback block, not the name anyone sees. The VISIBLE name on
   their slide is `.fsTitle`, measured live at 1324px:

     Anton 47px / 67px, weight 400, UPPERCASE, rgb(0,38,99), 15px below

   which is what this theme had in the first place. Read the visible
   element, not the first one whose class sounds right.

   THE ROLE IS NOT SHOWN. Their visible titles are "OLIVIA DUGGER '21",
   "KYLE MOLETT", "WAYNE WARREN '79", "RYDER SMITH '26" -- name and class
   year only. "Alumni" / "Student" lives only in the hidden block. The
   theme prints whatever the post title is, so a school can put a role
   there if it wants one; the bdcs demo data matches what they display. */
.voice__name {
  font-family: var(--font-display);
  font-size: clamp(2rem, 3.6vw, 47px); line-height: 1.426; font-weight: 400;
  color: var(--c-primary);
  text-transform: uppercase; margin-bottom: 15px;
}
/* Their quote is 19px/34px Poppins 400 ITALIC in rgb(82,82,82), with 20px
   between paragraphs. Ours ran 17px/28px roman, which is the difference
   Brian could see without measuring anything. */
.voice__quote { font-style: italic; color: var(--c-gray); font-size: 19px; line-height: 34px; }
.voice__quote p { margin: 0 0 20px; }
.voice__quote p:last-child { margin-bottom: 0; }

/* Controls: their `.controls-wrapper` is a right-aligned flex ROW carrying,
   in order, a thin horizontal rule, a bordered "More Voices" button, and two
   54x54 circular arrows in the secondary color. Measured on bdcs.org at
   1324px inside their 1180px row:

     rule     237 x 1px, rgb(205,205,205), then a 17px gap
     button   see `.voices__more`, then a 20px gap
     arrows   54px circles, 8px apart

   The rule is a `::before` here rather than their absolutely-positioned one
   at `left: 630px; right: 313px` -- an absolute rule pinned to two hard
   pixel offsets only lines up at their exact width. As a shrinkable flex
   item it holds 237px on the desktop and gives way instead of colliding. */
.voices__controls {
  display: flex; align-items: center; gap: 8px;
  justify-content: flex-end; margin-top: 40px;
}
.voices__controls::before {
  content: "";
  flex: 0 1 237px;
  height: 1px;
  /* Their own hairline, and it is darker than this theme's `--c-line`
     (#e5e5e5), so it is written out rather than borrowed. */
  background: #cdcdcd;
  margin-right: 9px; /* 9 + the 8px flex gap = their 17px */
}
.voices__controls .arrow-btn {
  width: 54px; height: 54px;
  background: var(--c-secondary);
}
.voices__controls .arrow-btn:hover,
.voices__controls .arrow-btn:focus { background: var(--c-primary-dark); }
/* "MORE VOICES" -- a bordered button, measured on their live anchor:
   Poppins 14px/28px weight 700, uppercase via `text-transform` (their DOM
   text is "More Voices"), rgb(0,38,99) text on a transparent ground, 1px
   solid the same navy, 3px radius, padding 20px 26px 19.5px, 0.5s
   transition. The navy is this theme's `--c-primary`, which the bdcs demo
   profile already resolves to #002663, so nothing is hardcoded here.

   `inline-block`, not their `inline`: theirs leaves a 61.5px border box on
   a 29px line box, so the button's own frame overlaps the row around it.
   That is their bug, not a style to copy.

   HOVER IS ASSUMED, not measured -- the browser harness would not deliver a
   real pointer to their anchor. It follows this theme's existing bordered
   button (`.btn--box`): fill solid in the border color with white text, at
   their measured 0.5s. */
.voices__more {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 14px; line-height: 28px; font-weight: 700;
  text-transform: uppercase; color: var(--c-primary);
  text-decoration: none;
  background: transparent;
  border: 1px solid var(--c-primary);
  border-radius: 3px;
  padding: 20px 26px 19.5px;
  margin-right: 12px; /* 12 + the 8px flex gap = their 20px */
  transition: background-color 0.5s ease, color 0.5s ease, border-color 0.5s ease;
}
.voices__more:hover, .voices__more:focus {
  background: var(--c-primary);
  border-color: var(--c-primary);
  color: var(--c-white);
  text-decoration: none;
}

/* No JS: the track cannot move, so the voices stack and every quote stays
   readable -- the same base-state-first approach as the values band. */
html:not(.has-js) .voices__track { display: block; }
html:not(.has-js) .voice { margin-bottom: 48px; }
html:not(.has-js) .voices__controls { display: none; }

@media (max-width: 900px) {
  .voice { flex-wrap: wrap; gap: 28px; }
  .voice__photo { flex-basis: 100%; }
  .voice__photo img { height: 420px; }
}

/* ==========================================================================
   Fast facts
   ========================================================================== */
.facts { position: relative; isolation: isolate; background-color: var(--c-primary-dark); padding: 0 0 72px; }
/* THE PHOTO IS ITS OWN LAYER so it can travel with the stats (Brian,
   2026-08-27: "we want the image to animate with the stats at the same
   time"). A `background-image` on the section cannot be transformed
   independently of the section, which is why the old build could only
   move the stat row. `.facts__media` clips so the rising inner layer
   never adds page height or overflow; `.facts__media-inner` carries the
   picture and is the element the entrance moves. */
.facts__media { position: absolute; inset: 0; overflow: hidden; z-index: 0; }
.facts__media-inner {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
}
/* Scrim sits above the photo layer and below the card and stats. It has an
   explicit z-index because `.facts__media` is a positioned sibling that
   comes LATER in the DOM -- without one, the generated ::before would paint
   underneath the photo and do nothing. */
.facts::before { content: ""; position: absolute; inset: 0; z-index: 1; background: rgba(0, 22, 46, 0.55); }
.facts__card {
  position: relative; z-index: 2;
  background: var(--c-white);
  /* MEASURED on bdcs.org 2026-08-27 at 1324px: their card is 660x317 with
     `padding: 60px 70px` and a 1px #ececec hairline border. Ours was 560
     wide at 48/56/40, which is what Brian meant by "fast fact box text is
     not the same as the original site". */
  max-width: 660px;
  padding: 60px 70px;
  border: 1px solid var(--c-line);
  /* THE LIFT IS A VARIABLE, not a literal, and that is load-bearing.
     This `transform` is LAYOUT -- it pulls the card up over the photo.
     The entrance animates the same property, so a bare `translateY()`
     in the entrance would silently overwrite the lift and drop the card
     56px. Every rule that moves this card composes with the variable. */
  --facts-card-lift: -56px;
  transform: translateY(var(--facts-card-lift));
  margin-bottom: -12px;
  /* Above `.facts__viewport` (also z-index 2) at every point in the
     entrance, including mid-flight when the two travel toward each other. */
  z-index: 3;
}
/* THE CARD'S OWN TYPE SCALE, not the shared `.eyebrow` / `.display-heading`.
   Both of those are used by five other sections; bdcs.org's fast-facts card
   runs a scale of its own and editing the shared classes would ripple
   across the page. Measured on their card:

     eyebrow  "MORE ABOUT US"  Poppins 21px / 25.2px, weight 700, uppercase,
                               rgb(82,82,82), 5px below
     heading  "FAST FACTS"     Anton 120px / 117px, weight 400, uppercase,
                               rgb(196,2,51) -- their SECONDARY, not their
                               navy, which is the same choice they make on
                               the core-values band

   `--c-secondary` falls back to `--c-primary` in the real theme, so Cristo
   Rey's own build is unchanged unless a school sets a secondary color.
   The heading is clamped rather than fixed at 120px so it still fits a
   phone; 120px is its ceiling, and 0.975 is their 117/120 line-height. */
.facts__card .eyebrow {
  font-size: 21px; line-height: 1.2; font-weight: 700;
  letter-spacing: normal; color: var(--c-gray);
  margin-bottom: 5px;
}
.facts__card .display-heading {
  color: var(--c-secondary);
  font-size: clamp(3rem, 9vw, 120px);
  line-height: 0.975;
  margin: 0 0 20px;
}
.facts__card::after { content: ""; display: block; width: 240px; height: 1px; background: var(--c-line); }

/* FOUR STATS PER SLIDE, sliding. MEASURED on bdcs.org 2026-08-27 at 1324px.

   Correction to an earlier reading of this module. Their track translates
   -1180px per click, which was taken to mean "one large stat at a time".
   It means one SLIDE, and a slide is a 1180px flex row of FOUR stats:
   three real slides (slick clones the rest for its loop), grouped 4 / 4 / 3.
   Confirmed by reading the slide text of every `.slick-slide`, including
   that the third slide really does hold only three stats.

     row        display: flex, gap: 20px
     stat       padding-right 49px, left aligned
     divider    1px x 46px in their gold, at `right: -10px; top: 50%`
                (their own rule -- the top EDGE sits at the midpoint, so
                the rule hangs below centre), hidden on the last stat
     number     Poppins 67px / 73px, weight 700, uppercase, white
     label      Poppins 16px / 23px, weight 600, white

   DELIBERATE DIFFERENCE: their four columns are content-sized
   (`flex: 0 1 auto`, so 221 / 371 / 322 / 205 on slide 0). Ours are equal
   (`flex: 1 1 0`) because the theme has to hold whatever a school writes
   -- content-sizing clumps the row to the left the moment the labels are
   shorter than Bishop Dunne's. Same type, same gaps, same dividers.

   Same viewport/track shape as `.voices`: the viewport clips, the track
   moves, individual stats never toggle display. */
.facts__viewport { position: relative; z-index: 2; overflow: hidden; }
.facts__grid {
  display: flex;
  transition: transform 1s ease;
  will-change: transform;
}
.facts__slide {
  flex: 0 0 100%;
  display: flex;
  gap: 20px;
  padding: 24px 0 40px;
}
.fact {
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  color: var(--c-white);
  text-align: left;
  padding-right: 49px;
  /* NO `hyphens: auto`. It was harmless while one stat owned the full
     width; in a four-up row the columns are narrow enough that it fires,
     and "unique colleges and univer-sities" reads as a typo rather than
     as typography. Long words wrap to the next line instead. */
  overflow-wrap: break-word;
}
.fact:not(:last-child)::after {
  content: "";
  position: absolute;
  right: -10px; top: 50%;
  width: 1px; height: 46px;
  background: var(--c-accent);
}
/* Their number is 67px/73px Poppins 700 uppercase white; the label
   16px/23px at weight 600. */
.fact__number {
  font-size: clamp(2.4rem, 5.06vw, 67px);
  line-height: 1.09; font-weight: 700;
  text-transform: uppercase;
  margin-bottom: 10px;
  /* A stat number must NEVER split from its UNIT. Brian caught "100" on one
     line with a lone "%" beneath it, caused by the cell's
     `overflow-wrap: break-word`, which is there for the label underneath.

     This used to be `white-space: nowrap`, which also banned wrapping at a
     SPACE -- so "$8.29 Million" had to be shrunk to fit, and it sat at 30px
     next to three numbers at 67px with nothing lining up. `overflow-wrap:
     normal` is the finer instrument: it forbids a break inside a token
     ("100%", "10:1") while still allowing one between words, so
     "$8.29 MILLION" wraps after the "$8.29" and stays at full size. That is
     exactly what bdcs.org's own row does. */
  overflow-wrap: normal;
  hyphens: manual;
}
/* While a stat is counting up (see factCounter in main.js), keep every
   digit the same width so the column does not wobble as values roll. The
   class comes off when the count settles, restoring the font's normal
   figures for the final rendered value. */
.fact__number.is-counting { font-variant-numeric: tabular-nums; }
/* Only for a number that is one long UNBREAKABLE token, which would
   otherwise run out of its quarter of the row. See the `--long` test in
   template-parts/facts.php: it measures the longest WORD, not the whole
   string, so a multi-word figure keeps the full 67px. */
.fact__number--long { font-size: clamp(1.2rem, 2.3vw, 30px); }
.fact__label { font-weight: 600; font-size: 16px; line-height: 23px; margin-top: 0; }

/* Their arrows: 54x54 circles in the secondary color, 8px apart, sitting
   under the stat rather than beside it. */
.facts__controls {
  position: relative; z-index: 2;
  display: flex; align-items: center; gap: 8px;
  justify-content: center; margin-top: 8px;
}
.facts__controls .arrow-btn {
  width: 54px; height: 54px;
  background: var(--c-secondary);
}
.facts__controls .arrow-btn:hover,
.facts__controls .arrow-btn:focus { background: var(--c-primary-dark); }

/* No JS: the track cannot move, so the stats stack and all of them stay
   readable. */
html:not(.has-js) .facts__grid { display: block; }
html:not(.has-js) .facts__controls { display: none; }

/* ==========================================================================
   Partner / college logo strip
   ========================================================================== */
/* Their module is 94px tall with `margin-top: 50px` and 20px of bottom
   padding; logos are capped at 54px. */
/* NO `margin-top`. It used to carry 50px, and because a margin is
   transparent the page's white showed through the gap between the grey
   news band ending and this strip starting -- the white band Brian
   pointed at on 2026-08-27 ("remove this white space here"). The breathing
   room is now padding INSIDE the strip, so the two sections meet with no
   background showing between them.

   The `border-top` went with it: a 1px #e5e5e5 rule between #f5f5f5 and
   #ffffff is the same faint horizontal line this section has twice been
   asked to stop drawing. The colour change between the two sections is
   the divider. */
.logo-strip { padding: 34px 0 24px; overflow: hidden; }
/* Label over the marquee. NOT from bdcs.org -- their strip carries no
   heading; this is Brian's addition (2026-08-27), so it should read as a
   caption for the strip rather than compete with the display headings
   above it. It reuses `.eyebrow` for exactly that reason: one small-label
   treatment across the theme instead of a second one invented here.

   `text-transform` is left to `.eyebrow`, but the sentence carries a
   trailing colon and normal sentence case in the Customizer default, so
   the copy still reads correctly if a school ever overrides the case.

   It sits outside `.logo-strip__track`, so it is not a flex item and
   cannot affect the marquee's width, its `-50%` loop, or the
   pause-on-hover rule, all of which are scoped to the track. */
.logo-strip__heading {
  margin: 0 0 14px;
  text-align: center;
  color: var(--c-gray);
}
/* FULL-BLEED (Brian, 2026-08-27: "make it full screen"): the marquee runs
   edge to edge, so the container stops constraining it here. The section's
   own overflow:hidden clips at the viewport, which is also what keeps the
   full-bleed track from creating horizontal page scroll. */
.logo-strip .container { max-width: none; padding: 0; overflow: hidden; }
.logo-strip__track {
  display: flex; flex-wrap: nowrap; align-items: center;
  gap: 0;
  width: max-content;
  /* bdcs.org's strip is a marquee too -- `js-marquee-wrapper` running
     `marqueeAnimation` for 89.28s over ~104 logos, which works out at
     roughly 150px/sec. Ours moves half a ~3000px track, so 10s gives
     the same apparent speed; at 30s it crawled by comparison. Matching
     the pixel rate rather than the duration is the point -- their loop
     is long because their list is long. */
  /* Slower than the bdcs-matched ~150px/s -- Brian: "make the school's
     logo bar slower." main.js sets the real duration from the track's
     rendered width at ~75px/s; this fixed value is only the no-JS
     fallback, sized for a typical 15-logo set. */
  animation: logo-strip-scroll 60s linear infinite;
}
.logo-strip:hover .logo-strip__track,
.logo-strip__track:focus-within { animation-play-state: paused; }
@keyframes logo-strip-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
.logo-strip__item {
  display: flex; align-items: center; justify-content: center;
  padding: 0 32px; height: 64px;
  border-left: 1px solid var(--c-line);
}
.logo-strip__item:first-child { border-left: 0; }
.logo-strip__item img {
  max-height: 54px; max-width: 130px; width: auto; height: auto;
  object-fit: contain;
}
@media (max-width: 760px) {
  .logo-strip__item { padding: 10px 18px; height: 52px; border-left: 0; }
  .logo-strip__item img { max-height: 34px; max-width: 96px; }
}

/* ==========================================================================
   News
   ========================================================================== */
/* News, measured on bdcs.org's `news-events` module 2026-08-26, re-measured
   and rebuilt 2026-08-27 (entrance, hover choreography, type scale).

   Their heading block is CENTRED like ours, at their type scale: eyebrow
   25px/53px Poppins 700 uppercase in near-black with a 20px gap under it,
   h2 120px/87px Anton (line-height 0.725), then 106px of air before the
   cards. Their cards are 496x402 with a 496x300 image and -- unlike a
   default card -- NO white background and NO shadow: the image and text
   sit directly on the page.

   THREE DELIBERATE DIFFERENCES FROM bdcs.org, all of them Brian's call:

   1. THE BACKGROUND IS CONTINUOUS. Theirs is not: `.news-events::before`
      is a partial panel (`top:170px; bottom:0`, #F5F5F5) sitting over a
      full-height `::after` that paints a diagonal pinstripe SVG, so a
      hard horizontal seam runs straight through their "NEWS & EVENTS"
      wordmark with white and a stripe corner above it. Verified live at
      1324px on 2026-08-27. Brian called that seam a bug, so ours keeps
      the plain `.section--light` fill behind the whole section.

   2. NO CAROUSEL, SO NO CLIPPED CARD. Their row is a 5-item slick slider
      whose 496px slides overflow a 1324px viewport, which is why their
      leftmost card is always a sliced fragment at different height from
      its neighbours. Ours is a contained 3-up grid: every card shares a
      top edge and nothing is cut off.

   3. THE TYPE IS SCALED TO OUR CARD, NOT COPIED IN PIXELS. Their title is
      26px/38px on a 496px card; our contained grid gives a 389px card, so
      the same ratio lands at 20px/30px. Weight is 700 rather than their
      800 because the theme's Poppins subset stops at 700.

   Kept exactly: image ratio 496/300, `object-position` center top, the
   20px gap from image to title, the 20px text indent, the 53px row gap,
   and every hover value (see the hover block below). */
/* `padding-bottom` is stated, not inherited. `.section` gives 88px, which
   stacked on top of `.news__more`'s own 53px and, until 2026-08-27, a
   50px transparent margin on the strip below -- three separate spacings
   piling up under the button. bdcs.org measures 32px of air under their
   own "More news" button before the section ends, and 64px here lands the
   same total gap from the bottom of the card text to the section edge
   (53 + button + 64 vs their 53 + 32 + button + 32). */
.news { text-align: center; padding-top: 100px; padding-bottom: 64px; overflow-x: clip; }
.news__head .eyebrow {
  font-size: 25px; line-height: 53px; font-weight: 700;
  color: var(--c-ink); text-transform: uppercase;
  margin-bottom: 20px;
}
.news__head .display-heading { font-size: clamp(3rem, 9vw, 7.5rem); line-height: 0.725; }
.news__grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 53px 12px;
  margin-top: 106px; text-align: left;
  /* Their news row is WIDER than the shared container: 496px cards, 12px
     apart (3 x 496 + 2 x 12 = 1512), bleeding past the 1180 content
     field. Ours scaled the cards down to fit the container (389px) and
     Brian could see it: "the news cards are bigger on theirs"
     (2026-08-27). This breakout gives the row up to 1512px, centered,
     with 24px viewport gutters -- on a large desktop the cards render at
     their exact 496x300; at 1324 they get 417, still bigger than before.
     Margin math, NOT transform: the entrance animation owns transform on
     this element. `.news` clips overflow-x already. */
  width: min(100vw - 48px, 1512px);
  margin-left: calc((100% - min(100vw - 48px, 1512px)) / 2);
}
.news-card { background: transparent; box-shadow: none; text-decoration: none; display: block; }
/* 496x300 on their card. `position: relative` is load-bearing: the gold
   bar, the navy scrim and the "View Story" badge are all absolutely
   positioned inside this box, and `overflow: hidden` is what keeps the
   parked badge and the 1.2x image out of sight. */
.news-card__img {
  position: relative;
  aspect-ratio: 496 / 300; overflow: hidden; background: var(--c-bg-light);
}
.news-card__img img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: center top;
  transition: transform 0.5s ease;
}
.news-card__body { padding: 20px 0 0 20px; }
.news-card__date { font-size: 0.8rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-gray-light); }
/* Their card title is #525252 (`--c-gray`), NOT the near-black `--c-ink`
   this used to use, and it turns `--c-secondary` on hover over 0.5s. */
.news-card__title {
  /* Their real title: 26px/38px weight 800 (re-measured live 2026-08-27
     on the 496px card -- the earlier 20px was our own scale-down for the
     narrower card, overridden by Brian's side-by-side). */
  font-size: 26px; line-height: 38px; font-weight: 800;
  color: var(--c-gray); margin: 6px 0 0;
  transition: color 0.5s ease;
}

/* --------------------------------------------------------------------
   News card hover -- MEASURED off bdcs.org 2026-08-27 by driving a real
   hover on their live card and reading the result, not by reading rules.

   Five things move together, all 0.5s:
     1. img            transform: scale(1) -> scale(1.2)
     2. ::before       an 8px gold bar across the bottom, opacity 0 -> 1
     3. ::after        a navy scrim, linear-gradient(transparent 0%,
                       <primary> 90.41%), opacity 0 -> 1, which is what
                       makes the white label legible over a photo
     4. .news-card__cta  "VIEW STORY" + its white circular badge slides in
                       from off the left edge to its resting position
     5. title          colour -> --c-secondary

   Their measured resting/hover pairs: image scale 1 -> 1.2; gold bar
   8px tall at the image's bottom, z-index 12; scrim full-size, z-index
   10; the read-more link 14px/28px Poppins 700 white uppercase with
   padding-left 40px, z-index 12, parked at translateX(-226.856px) and
   landing at translateX(0) 32px in from the card's left edge and 42px
   above the image's bottom; the white badge circle 36x36 at left:-10px
   bottom:-5px, z-index 5, carrying a 12px arrow glyph in the secondary
   colour. Ours reproduces all of it; the parked distance and the inset
   are expressed against our narrower card.

   "View Story" is literal source text on their site, uppercased in CSS,
   so ours is a translatable string uppercased the same way. */
.news-card__cta {
  position: absolute; left: 26px; bottom: 14%;
  z-index: 12;
  display: block; padding-left: 40px;
  font-size: 14px; line-height: 28px; font-weight: 700;
  color: var(--c-white); text-transform: uppercase; white-space: nowrap;
  /* Parked well clear of the card's left edge. `.news-card__img` clips it,
     so an over-generous distance costs nothing and guarantees the badge
     never peeks out at a narrow card width. */
  transform: translateX(-280px);
  transition: transform 0.5s ease;
}
.news-card__cta::before {
  content: ""; position: absolute; left: -10px; bottom: -5px;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--c-white); z-index: 5;
}
/* Their glyph is an IcoMoon arrow, which this theme does not ship. Drawn
   from borders instead, in the same secondary colour at the same size. */
.news-card__cta::after {
  content: ""; position: absolute; left: 4px; top: 50%;
  width: 9px; height: 9px; margin-top: -5px;
  border-right: 2px solid var(--c-secondary);
  border-bottom: 2px solid var(--c-secondary);
  transform: rotate(-45deg);
  z-index: 6;
}
.news-card--has-media .news-card__img::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 8px;
  background: var(--c-accent); z-index: 12;
  opacity: 0; transition: opacity 0.5s ease;
}
.news-card--has-media .news-card__img::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(rgba(0, 0, 0, 0) 0%, var(--c-primary) 90.41%);
  z-index: 10;
  opacity: 0; transition: opacity 0.5s ease;
}
.news-card:hover .news-card__img img,
.news-card:focus-visible .news-card__img img { transform: scale(1.2); }
.news-card--has-media:hover .news-card__img::before,
.news-card--has-media:focus-visible .news-card__img::before,
.news-card--has-media:hover .news-card__img::after,
.news-card--has-media:focus-visible .news-card__img::after { opacity: 1; }
.news-card--has-media:hover .news-card__cta,
.news-card--has-media:focus-visible .news-card__cta { transform: translateX(0); }
.news-card:hover .news-card__title,
.news-card:focus-visible .news-card__title { color: var(--c-secondary); }

/* No hover on a touch screen, so the revealed state IS the resting state
   there -- same call already made for the program tiles. bdcs.org leaves
   its own badge parked off-screen forever on a phone, which is a dead end
   rather than a design. The scrim only darkens the lower half of the
   photo, so the card still reads as a photo. */
@media (hover: none) {
  .news-card--has-media .news-card__img::before,
  .news-card--has-media .news-card__img::after { opacity: 1; }
  .news-card--has-media .news-card__cta { transform: translateX(0); }
}

/* Their button is centred under the grid. */
.news__more { margin-top: 53px; display: flex; justify-content: center; }
/* bdcs.org uses the SAME boxed button in two colours: red in the welcome
   section and navy here (measured rgb(0, 38, 99) on their "More news").
   `.btn--box` defaults to the secondary, so this one is pulled back to
   the primary rather than a second button class being invented.
   Their own footer sits the button to the RIGHT with the carousel arrows
   beside it; ours has no arrows, so it stays centred. */
.news__more .btn--box { border-color: var(--c-primary); color: var(--c-primary); }
.news__more .btn--box:hover,
.news__more .btn--box:focus { background: var(--c-primary); border-color: var(--c-primary); color: var(--c-white); }

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer {
  position: relative;
  background: var(--c-navy);
  background-size: cover; background-position: center right;
  background-blend-mode: soft-light;
  color: var(--c-white);
  /* Their `.footer-container` is padded 90px top / 68px bottom. */
  padding: 90px 0 68px;
}
.site-footer::before { content: ""; position: absolute; inset: 0; background: rgba(0, 33, 66, 0.82); }
.site-footer a { color: var(--c-white); }
.site-footer__inner { position: relative; z-index: 2; }
.site-footer__cols {
  display: flex; flex-wrap: wrap; gap: 48px;
  padding-bottom: 40px;
}
.site-footer__brand { flex: 0 0 260px; border-right: 1px solid rgba(255,255,255,0.25); padding-right: 48px; }
.site-footer__brand img { max-width: 100%; }
/* Brian 2026-08-27: bigger footer logo. The dedicated footer logo fills the
   brand column (260px minus the 48px divider gap = 212px wide for the
   school's wide wordmark, up from a 100px cap); max-height still guards a
   square/tall logo file. */
.site-footer__brand .site-footer__logo { max-width: 100%; max-height: 160px; width: auto; height: auto; }
.site-footer__brand .site-title { font-family: var(--font-display); color: var(--c-white); font-size: 1.5rem; text-transform: uppercase; text-decoration: none; }
/* Measured on their footer: column headings 20px/22px Poppins 700 uppercase
   in their gold, with no extra letter-spacing; body links 16px/24px at
   weight 700; the phone line 17px/32px at weight 500. Ours ran 1.15rem with
   0.04em tracking. */
.site-footer h4 {
  color: var(--c-accent);
  font-size: 20px; line-height: 22px; font-weight: 700; letter-spacing: normal;
  text-transform: uppercase; margin-bottom: 20px;
}
.footer-links a, .site-footer__utility a { font-size: 16px; line-height: 24px; font-weight: 700; }
.site-footer address, .site-footer address a { font-size: 17px; line-height: 32px; font-weight: 500; }
.site-footer address { font-style: normal; line-height: 1.9; }
.site-footer address a { text-decoration: underline; }
.footer-links ul { list-style: none; margin: 0; padding: 0; }
.footer-links li { margin: 8px 0; }
.footer-links a { text-decoration: none; }
.footer-links a:hover { color: var(--c-accent); }
.social-nav { display: flex; gap: 12px; margin-top: 22px; }
.social-nav a {
  width: 46px; height: 46px; border: 1px solid rgba(255,255,255,0.5);
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none; font-size: 1rem;
  transition: background 0.2s, border-color 0.2s;
}
.social-nav a:hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-ink); }
.site-footer__mission { max-width: 640px; margin: 8px 0 32px; color: rgba(255,255,255,0.92); }
.site-footer__utility { display: flex; flex-wrap: wrap; gap: 26px; font-size: 0.9rem; }
.site-footer__utility li, .menu-overlay .utility-links li { list-style: none; margin: 0; padding: 0; }
.site-footer__utility a { text-decoration: none; }
.site-footer__utility a:hover { color: var(--c-accent); }
.site-footer__credit { margin-top: 28px; font-size: 0.8rem; color: rgba(255,255,255,0.65); }

/* ==========================================================================
   Inner pages
   ========================================================================== */
.page-banner {
  position: relative;
  background: var(--c-primary);
  background-size: cover; background-position: center;
  padding: 84px 0 56px;
  color: var(--c-white);
}
.page-banner::before { content: ""; position: absolute; inset: 0; background: rgba(0, 40, 82, 0.55); }
.page-banner .container { position: relative; z-index: 2; }
.page-banner h1 { color: var(--c-white); margin: 0; }
.breadcrumb { font-size: 0.85rem; margin-bottom: 10px; color: rgba(255,255,255,0.85); }
.breadcrumb a { color: var(--c-accent); text-decoration: none; }
.breadcrumb .sep { margin: 0 8px; opacity: 0.6; }

.page-layout { display: flex; gap: 56px; padding: 64px 0 88px; }
.page-layout__content { flex: 1; min-width: 0; }
.page-layout__content > :first-child { margin-top: 0; }
.page-layout__aside { flex: 0 0 280px; }
.section-nav { border-top: 4px solid var(--c-accent); background: var(--c-bg-light); padding: 26px 26px 30px; position: sticky; top: 24px; }
.section-nav h2 { font-family: var(--font-body); font-size: 1rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-primary); }
.section-nav ul { list-style: none; margin: 0; padding: 0; }
.section-nav li { border-bottom: 1px solid var(--c-line); }
.section-nav a { display: block; padding: 10px 2px; text-decoration: none; font-size: 0.95rem; }
.section-nav .current_page_item > a, .section-nav .current-menu-item > a { font-weight: 700; }

.entry-content a { text-decoration: underline; }
.entry-content h2 { font-size: 1.9rem; color: var(--c-primary); margin-top: 1.4em; }
.entry-content h3 { margin-top: 1.3em; }
.entry-content blockquote {
  border-left: 4px solid var(--c-accent);
  margin: 1.5em 0; padding: 4px 0 4px 22px;
  font-style: italic; color: var(--c-gray);
}
.entry-content table { border-collapse: collapse; width: 100%; }
.entry-content th, .entry-content td { border: 1px solid var(--c-line); padding: 10px 14px; text-align: left; }
.entry-content th { background: var(--c-bg-light); }
.wp-caption { max-width: 100%; }
.wp-caption-text { font-size: 0.85rem; color: var(--c-gray); }
.alignleft { float: left; margin: 0 24px 16px 0; }
.alignright { float: right; margin: 0 0 16px 24px; }
.aligncenter { margin-left: auto; margin-right: auto; }

/* Blog index / archives */
.post-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; padding: 64px 0 88px; }
.archive-title { padding: 0; }
.pagination { grid-column: 1 / -1; display: flex; gap: 10px; justify-content: center; margin-top: 20px; }
.pagination .page-numbers {
  display: inline-block; min-width: 42px; text-align: center;
  padding: 9px 12px; border: 1px solid var(--c-line);
  text-decoration: none; color: var(--c-ink);
}
.pagination .page-numbers.current { background: var(--c-primary); color: var(--c-white); border-color: var(--c-primary); }

/* Single post */
.single-article { max-width: 800px; margin: 0 auto; padding: 64px 24px 88px; }
.single-article .entry-meta { color: var(--c-gray); font-size: 0.9rem; margin-bottom: 28px; }
.single-article .post-thumbnail { margin-bottom: 32px; }

/* ==========================================================================
   Scroll reveal (fade-up entrance for the sections below the hero)

   Hidden state only applies under `html.has-js` (a class set by a tiny
   inline script at the top of <head> -- see header.php). No JS or a
   blocked script means the class is never added, so content is fully
   visible immediately: this is a progressive enhancement, not a gate.
   `.is-visible` is added by main.js the first time a section scrolls
   into view (IntersectionObserver), or immediately on load for
   prefers-reduced-motion. nth-child rules stagger the repeating
   children once their parent section is revealed.
   ========================================================================== */
/* `.voices` and `.facts` are NOT in this group -- each has its own
   measured entrance further down this file. */
/* Welcome section reveal, corrected 2026-08-26 to Brian's own direct
   observation of the live site, which overrides an earlier finding built
   from a static cross-origin CSS rule search (that search reported one
   single .welcome__head block sliding in as a unit -- wrong per Brian
   watching the actual page twice: the heading, body copy and button
   animate as three separate, sequenced moves, not one).
     1. Eyebrow + heading slide in LEFT -> RIGHT (from off-left into
        place) and fire first, no delay.
     2. Body paragraph fades/slides in AFTER the heading has landed, not
        alongside it.
     3. The "Learn More" button slides in RIGHT -> LEFT (from off-right
        into place) -- the opposite direction from the heading -- and
        fires last.
   Each piece gets its own transition-delay so they read as a staged
   sequence off one shared `.welcome.is-visible` trigger, rather than
   needing three separate IntersectionObserver entries in main.js. */
html.has-js .welcome__head .eyebrow,
html.has-js .welcome__head .display-heading {
  opacity: 0;
  transform: translateX(-250px);
  transition-property: opacity, transform;
  transition-duration: 0.9s;
  transition-timing-function: ease;
}
html.has-js .welcome__head .section-intro {
  opacity: 0;
  transform: translateX(-60px);
  transition-property: opacity, transform;
  transition-duration: 0.7s;
  transition-timing-function: ease;
  transition-delay: 0.5s;
}
html.has-js .welcome__head .welcome__cta {
  opacity: 0;
  transform: translateX(250px);
  transition-property: opacity, transform;
  transition-duration: 0.9s;
  transition-timing-function: ease;
  transition-delay: 0.85s;
}
html.has-js .welcome.is-visible .welcome__head .eyebrow,
html.has-js .welcome.is-visible .welcome__head .display-heading,
html.has-js .welcome.is-visible .welcome__head .section-intro,
html.has-js .welcome.is-visible .welcome__head .welcome__cta {
  opacity: 1;
  transform: translateX(0);
}

/* --------------------------------------------------------------------
   Voices band entrance -- MEASURED off bdcs.org 2026-08-27.

   Brian: "I want the same animations of the original site where the
   'hear from our bd voices' comes in right to left and the rest left to
   right." His description and the measurement agree exactly.

   Their module is `.posts-main-container`; the reveal class `is-in-view`
   sits on the inner `.posts-container`. Removing it and reading the rest
   state gave, for the four children:

     header (eyebrow + heading)   opacity 0, translateX( +100px)
     .fsElementContent (slider)   opacity 0, translateX( -100px)
     .controls-wrapper (arrows)   opacity 0, translateX( -100px)
     footer (empty on their page) opacity 0, no transform

   TWO THINGS WORTH KEEPING STRAIGHT. (1) The opposing directions are the
   whole effect: the heading travels LEFT into place while everything
   else travels RIGHT, and they cross. (2) There is NO stagger and no
   delay -- all of it moves together over `all 1.3s ease`. Staging it
   would destroy the crossing, which is the only thing that makes the
   opposition legible.

   1.3s is longer than the theme's usual 1s slide, and it is theirs, not
   a slip: 100px over 1.3s is a slow drift rather than a snap, which is
   what lets the two directions register at normal scroll speed.

   WHY THE VIEWPORT AND NOT THE TRACK: `.voices__track` owns `transform`
   for the carousel's own 1s slide between quotes. Animating it here
   would put two transforms on one axis and could strand a quote
   mid-travel if someone scrolls in during a slide change. The viewport
   is the parent, so the two compose instead of fighting -- the same
   container-not-children rule the tiles and values bands follow.

   OVERFLOW: `.voices` already carries `overflow: hidden`, which clips
   BOTH edges. That matters more than usual here because this is the
   first entrance in the theme that parks anything off to the LEFT.
   -------------------------------------------------------------------- */
html.has-js .voices__head {
  opacity: 0;
  transform: translateX(100px);
  /* Longhand on purpose -- the shorthand also resets transition-delay. */
  transition-property: opacity, transform;
  transition-duration: 1.3s;
  transition-timing-function: ease;
}
html.has-js .voices__viewport,
html.has-js .voices__controls {
  opacity: 0;
  transform: translateX(-100px);
  transition-property: opacity, transform;
  transition-duration: 1.3s;
  transition-timing-function: ease;
}
html.has-js .voices.is-visible .voices__head,
html.has-js .voices.is-visible .voices__viewport,
html.has-js .voices.is-visible .voices__controls {
  opacity: 1;
  transform: translateX(0);
}
/* `.facts__viewport`, not the individual stats: the facts track owns
   `transform` for its 1s slide, so revealing the children would put two
   animations on the same axis and could strand a stat mid-travel. Same
   container-not-children rule as the tiles, values and voices. */
/* --------------------------------------------------------------------
   News band entrance -- MEASURED off bdcs.org 2026-08-27, replacing the
   shared fade-up plus a 70ms-per-card stagger.

   Brian: the heading block enters right to left and the card row enters
   left to right, as a clear opposing pair. Their `news-events` module
   does exactly that, and removing its `is-in-view` class hands over the
   rest state, which IS the spec:

     header (eyebrow + h2)        opacity 0, translateX(+100px)
     .fsElementContent (cards)    opacity 0, translateX(-100px)
     .footerdivloc (More news)    opacity 0, translateX(-100px)
     all three                    1.3s ease, 0s delay

   NO STAGGER ANYWHERE. All three transition-delays measured 0s, and all
   three durations 1.3s -- the crossing IS the effect, so they share a
   start time. This is the third module where inventing a stagger would
   have been wrong, and it is also what produced the "cards do not share
   a baseline" complaint: the old 0/70/140ms delays on translateY(24px)
   left the three cards at three different heights for most of the
   entrance. One move on the container fixes that by construction.

   WHY THE CONTAINERS AND NOT THE CARDS: same rule as the tiles, values,
   voices and facts. The row moves as one unit, so one transform on
   `.news__grid` beats three on its children.

   `.news` carries `overflow-x: clip` (see the News block above) because
   the heading parks 100px to the RIGHT of its final position, which
   would otherwise put 100px of horizontal scroll on the document at
   every width. `clip` rather than `hidden` so no scroll container is
   created and `overflow-y` stays visible. */
html.has-js .news__head {
  opacity: 0;
  transform: translateX(100px);
  transition-property: opacity, transform;
  transition-duration: 1.3s;
  transition-timing-function: ease;
}
html.has-js .news__grid,
html.has-js .news__more {
  opacity: 0;
  transform: translateX(-100px);
  transition-property: opacity, transform;
  transition-duration: 1.3s;
  transition-timing-function: ease;
}
html.has-js .news.is-visible .news__head,
html.has-js .news.is-visible .news__grid,
html.has-js .news.is-visible .news__more {
  opacity: 1;
  transform: translateX(0);
}

/* --------------------------------------------------------------------
   Fast facts entrance -- MEASURED off bdcs.org 2026-08-27.

   Brian: "the fast facts box animates in from the top to the bottom and
   the other section from the bottom to the top at the same time." Their
   `stats-container` does exactly that, so his description and the
   measurement agree. Read off the rest state:

     header (their white card)   opacity 0, translateY(-200px)
     .wrapper-container (photo)  opacity 0, translateY(+200px)
     both                        all 1.3s ease, 0s delay

   THE TWO CONVERGE, and that is the whole effect -- so they share a
   start time and a duration. Staggering them would turn a convergence
   into two unrelated moves.

   WHAT MAPS ONTO WHAT -- corrected 2026-08-27 on Brian's instruction.
   Their rising element is the PHOTO; their stat row measures
   `transition-duration: 0s` and simply appears. Ours used to be the
   reverse: the stat row rose and the photo, being a section
   `background-image`, could not move at all. Brian: "on the fast facts
   we want the image to animate with the stats at the same time."

   So the photo was pulled out into `.facts__media-inner`, and the photo,
   the stat row and the arrows now share ONE rise -- same +200px, same
   1.3s, same 0s delay -- so they travel as a single unit while the card
   comes down to meet them. That is a deliberate superset of bdcs.org,
   which animates only the photo half; it is what Brian asked for and it
   reads as the same convergence.

   NO LAYOUT JUMP: `transform` never affects layout, so the card starting
   256px high (200 of travel plus its own 56px lift) and the stats
   starting 200px low change no page height and push nothing. The only
   mid-flight effect is paint overlap with the section above, by an
   element still near opacity 0 -- exactly what bdcs.org does.
   -------------------------------------------------------------------- */
html.has-js .facts__card {
  opacity: 0;
  /* Composes with the layout lift rather than replacing it. */
  transform: translateY(calc(var(--facts-card-lift) - 200px));
  transition-property: opacity, transform;
  transition-duration: 1.3s;
  transition-timing-function: ease;
}
html.has-js .facts__media-inner,
html.has-js .facts__viewport,
html.has-js .facts__controls {
  opacity: 0;
  transform: translateY(200px);
  transition-property: opacity, transform;
  transition-duration: 1.3s;
  transition-timing-function: ease;
}
html.has-js .facts.is-visible .facts__card {
  opacity: 1;
  transform: translateY(var(--facts-card-lift));
}
html.has-js .facts.is-visible .facts__media-inner,
html.has-js .facts.is-visible .facts__viewport,
html.has-js .facts.is-visible .facts__controls {
  opacity: 1;
  transform: translateY(0);
}

/* Program tiles entrance, corrected 2026-08-26. Two pieces of direction
   from Brian, both verbatim:

     1. "it comes in right to left" -- and, on watching it, that it
        "does not animate right to left, it seems to come from bottom to
        top quickly." He was exactly right: the tiles were still on the
        shared fade-up above (translateY(24px) at 0.55s), which is
        literally bottom-to-top and quick.
     2. "looks like they are trying to come in one by one -- they can
        all come in in one container." So NO per-tile stagger. The four
        tiles arrive together, as one block.

   Both are satisfied by moving ONE element -- the track -- instead of
   moving each tile. It starts offset to the right of its final position
   and travels leftward into place, at the 0.9s pacing Brian approved on
   the values card rather than the old 0.55s. Facts and news cards are
   untouched and keep their fade-up.

   bdcs.org's own tiles move on translateY because they are slick
   carousel slides; that is noted in the >=800px block above and is NOT
   what is copied here. Brian asked for right-to-left as one group, and
   that is what this does.

   WHY THE TRACK AND NOT THE TILES -- this was measured, not assumed.
   `.tiles__track` is a real horizontal scroller (`overflow-x: auto`,
   `scroll-snap-type: x mandatory`). Transformed overflow counts toward a
   scroll container's scrollWidth in the inline-end direction, so parking
   the TILES to the right grows the track's own scrollable width, and a
   mandatory-snap container re-anchors when that happens. Measured live
   at 375px on 2026-08-26 with a per-tile translateX(160px): the track
   loaded already sitting at `scrollLeft: 160` with the first tile partly
   off the left edge, before the visitor touched anything. Transforming
   the scroll container itself has no such effect -- an element's own
   transform does not feed its own scrollWidth -- so the carousel stays
   at scrollLeft 0 throughout.

   The rightward park does push the track past the section's right edge,
   which is where the document-level horizontal-overflow bug lives (the
   values band cost 136px of page overflow at 375px learning this). So
   `.tiles` carries `overflow-x: clip` -- `clip` rather than `hidden`
   because it does not create a scroll container, and it is the one
   overflow value that legally pairs with `overflow-y: visible`. */
html.has-js .tiles__track {
  opacity: 0;
  transform: translateX(160px);
  transition-property: opacity, transform;
  transition-duration: 0.9s;
  transition-timing-function: ease;
}
html.has-js .tiles.is-visible .tiles__track {
  opacity: 1;
  transform: translateX(0);
}

/* The 70ms-per-item stagger increments that used to live here are gone.
   `.tile` was dropped 2026-08-26 (Brian asked for the tiles to "all come
   in in one container"), `.fact` on the same day when the stats became a
   one-at-a-time carousel, and `.news-card` on 2026-08-27 when the news
   row was measured off bdcs.org and turned out to have no stagger at all
   -- it was also what made the three cards sit at three different heights
   through most of the entrance. Nothing staggers on this page now. */

/* --------------------------------------------------------------------
   Values band entrance -- REBUILT 2026-08-27 from bdcs.org's own module,
   replacing the right-to-left staged version built on 2026-08-26.

   WHY IT CHANGED. The old rule came from Brian's "it comes in right to
   left", which turned out to describe the PROGRAM TILES, not this band.
   The tiles are still right-to-left and must stay that way. He then said
   of this section: "the animation should be coming in from the bottom to
   the top not the side."

   So this was MEASURED off bdcs.org rather than guessed at, the same way
   their cross-fade curve was. Their module is `.image-switch`, and its
   reveal is driven by one class, `is-in-view`. Sampled live 2026-08-27 by
   removing that class, letting it settle, re-adding it and reading
   `getComputedStyle` every ~108ms:

     rest      opacity 0,  translateY(60px)
     entered   opacity 1,  translateY(0)
     timing    all 1s ease, 0.3s delay (nothing moves before ~300ms;
               settled by ~1300ms)
     flight    0.108 -> 13.7%, 0.235 -> 35.2%, 0.451 -> 74.8% travelled,
               which is CSS `ease` within a couple of points -- the same
               one easing the whole theme uses.

   THE FINDING THAT CHANGED THE SHAPE OF THIS BLOCK: **their entrance has
   no stagger at all.** The whole section moves as ONE element. Their
   slider stage, their card and their value list each measured
   `transform: none` with a 0s transition duration -- not one of them
   carries an entrance of its own; they simply ride the section up. The
   staged two-beat version here was our invention, not theirs, so it is
   gone rather than merely flipped onto the Y axis.

   Animating the SECTION also fixes the old hazard for free. The previous
   block deliberately avoided animating `.values__slide` because the
   slides own `opacity` for the 0.5s cross-fade and the two would fight.
   A parent's opacity multiplies with its children's, so it can never
   strand a slide part-faded.

   The delay lives on the `.is-visible` state, not the rest state, because
   that is where bdcs.org carries it: entering waits 0.3s, leaving would
   not. Transition values are read from the destination state.
   -------------------------------------------------------------------- */
html.has-js .values {
  opacity: 0;
  transform: translateY(60px);
  /* Longhand on purpose -- the shorthand also resets transition-delay. */
  transition-property: opacity, transform;
  transition-duration: 1s;
  transition-timing-function: ease;
}
html.has-js .values.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 0.3s;
}

@media (prefers-reduced-motion: reduce) {
  html.has-js .welcome__head,
  /* `.values` itself, not its children -- the section is the animated
     element as of 2026-08-27. Miss this and the whole band stays at
     opacity 0 for every reduced-motion visitor. */
  html.has-js .values,
  html.has-js .voices__head,
  html.has-js .voices__viewport,
  html.has-js .voices__controls,
  html.has-js .news__head,
  html.has-js .tiles__track,
  html.has-js .facts__media-inner,
  html.has-js .facts__viewport,
  html.has-js .facts__controls,
  html.has-js .news__grid,
  html.has-js .news__more {
    opacity: 1 !important;
    transform: none !important;
    transition-delay: 0ms !important;
  }
  /* NOT `transform: none` -- this card's transform is LAYOUT (the lift
     over the photo), not decoration. Blanking it drops the card 56px and
     breaks the overlap the design depends on. */
  html.has-js .facts__card {
    opacity: 1 !important;
    transform: translateY(var(--facts-card-lift)) !important;
    transition-delay: 0ms !important;
  }
  /* The value cross-fade is a state change, not decoration -- switching
     values must still work with reduced motion, it just must not animate.
     So the slides are NOT in the list above (that would force all six
     visible at once); their transition is collapsed to an instant swap
     instead. */
  html.has-js .values__slides > .values__slide,
  html.has-js .voices__track,
  html.has-js .facts__grid,
  /* The news card hover is a state change, not decoration -- it must still
     work, it just must not animate. Duration is collapsed rather than the
     transform being blanked: `transform: none` on `.news-card__cta` would
     un-park the badge and leave it sitting on every card permanently. */
  .news-card__img img,
  .news-card__cta,
  .news-card__title,
  .news-card--has-media .news-card__img::before,
  .news-card--has-media .news-card__img::after {
    transition-duration: 0.01ms !important;
  }
  /* The card rides the slide's state, so it must swap instantly too --
     and never keep a transform that could park it off-screen. */
  html.has-js .values__slides > .values__slide .values__panel {
    transform: none !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 1240px) {
  /* bdcs.org's 215px track gutter only works while there is room for it.
     Below their desktop width the gutter shrinks and the arrows follow it
     in, so the tiles never get squeezed into a sliver. The arrows are
     dropped entirely on phones, where the track is swiped rather than
     clicked and 55px circles would cover a tile. */
  .tiles__track { padding-left: 120px; scroll-padding-left: 120px; }
  .tiles__nav { left: 34px; }
}
@media (max-width: 900px) {
  .tiles__track { padding-left: 72px; scroll-padding-left: 72px; }
  .tiles__nav { left: 8px; }
}
@media (max-width: 760px) {
  .tiles__track { padding-left: 20px; scroll-padding-left: 20px; }
  .tiles__nav { display: none; }
}

@media (max-width: 1000px) {
  .news__grid { grid-template-columns: repeat(2, 1fr); }
  .post-list { grid-template-columns: repeat(2, 1fr); }
  .voice { flex-wrap: wrap; }
  .voice__photo { flex-basis: 300px; }
}
/* Four stats across only works while there is room for four. The row wraps
   to two and then to one, and the dividers go with it -- a vertical rule
   between two wrapped rows reads as a mistake rather than as a divider. */
@media (max-width: 1000px) {
  .facts__slide { flex-wrap: wrap; row-gap: 32px; }
  .fact { flex: 1 1 42%; padding-right: 24px; }
  .fact:not(:last-child)::after { display: none; }
}
@media (max-width: 620px) {
  .fact { flex: 1 1 100%; }
}
@media (max-width: 760px) {
  :root { --header-h: 72px; } /* 44px logo cap + 14px padding top and bottom, matching the rule below */
  .section { padding: 64px 0; }
  .utility-nav { display: none; }
  .search-toggle { display: none; }
  .site-header__brand { padding: 14px 20px; }
  .site-header__brand img { max-height: 44px; }
  .hero__content { padding: 0 22px 40px; }
  .hero__controls { bottom: 150px; }
  .values__list { text-align: left; margin-top: 8px; }
  .values__list button { padding-right: 0; padding-left: 46px; }
  .values__list button::after { left: 0; right: auto; }
  /* Sets the LIFT, not the transform -- writing `transform` here would
     clobber the entrance's composed value at this breakpoint. */
  .facts__card { padding: 36px 28px 30px; --facts-card-lift: -36px; }
  .news__grid { grid-template-columns: 1fr; }
  .post-list { grid-template-columns: 1fr; }
  .page-layout { flex-direction: column-reverse; gap: 32px; }
  .page-layout__aside { flex-basis: auto; }
  .section-nav { position: static; }
  .site-footer__brand { border-right: 0; padding-right: 0; flex-basis: auto; }
  /* Keep the enlarged footer logo proportionate on a phone: cap it below
     full container width so it never spans edge to edge at 375px. */
  .site-footer__brand .site-footer__logo { max-width: 260px; }
  /* .tile flex-basis used to be narrowed to 280px here. Removed
     2026-08-26: the tile's own base is now 285px (bdcs.org's own small
     size, which it uses below 800px) with a single bump to 364px at
     >=800px, so this override only introduced a third width that agreed
     with neither. The track is a horizontal scroller, so a tile wider
     than a narrow phone is expected rather than a bug. */
}

