/* ==========================================================================
   ub-elements.css — element-scoped styles for reusable UB components
   University of Bridgeport

   WHY THIS FILE EXISTS
   Everything here used to live in home.css, a PAGE-named stylesheet. That was
   wrong in a WordPress theme, and it was wrong in a way that produced real
   bugs rather than just untidiness:

     - These are BLOCKS. An editor can drop ub/hero-carousel or ub/cta-band on
       any page, so styling them from a file called "home" means the block is
       only styled where someone remembered to load that file.
     - program.css and academics.css both open with a comment saying they
       depend on home.css for .display-*, .eyebrow-rule, .reveal, .btn-ghost,
       .sticky-cta and .scroll-progress. Every program and academics page was
       therefore loading a stylesheet named "home" purely to get primitives
       that were never homepage-specific.
     - The scroll-progress hairline was declared here, driven by home.js, and
       emitted by header.php only when ubridgeport_is_home_layout() was true.
       Three separate page-gates on one element, so it silently did nothing
       everywhere except the front page.

   THE RULE THIS FILE ESTABLISHES
   Style by element, not by page. A component's styles live with the component
   and load wherever the component can appear. Page-named stylesheets keep
   only what is genuinely unique to that page's layout.

   WHAT BELONGS HERE
   Cross-cutting primitives and chrome that any page may use:
     - Display typography (.display-xl/.display-lg), .eyebrow-rule,
       .section-lede — emitted by every UB section-block heading.
     - .reveal — the scroll-reveal primitive, used by every section block.
     - .btn-ghost — a button variant that works on any dark background.
     - .scroll-progress — site chrome, every page.
     - .sticky-cta — site chrome, any page that opts in.
     - .hero2-switcher / .hs-* — the hero carousel's slide control, which
       travels with ub/hero-carousel.

   WHAT DOES NOT BELONG HERE
   Section layout that is specific to one page's composition. .pathways,
   .outcomes, .centennial2 and friends stay in home.css until they are needed
   elsewhere; moving them now would be churn without a caller.

   SCOPING NOTE
   The primitives stay scoped under `body.v2` exactly as before. That class is
   applied by ubridgeport_uses_section_blocks(), and dropping the scope here
   would repaint core-block buttons and headings on pages that never opted in.
   Site chrome (.scroll-progress) is deliberately NOT scoped — it is not part
   of the v2 design layer, it is the whole site's.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Tokens this file needs that style.css does not define.

   These five were declared only in home.css — a PAGE stylesheet — while this
   file has been consuming them since it was split out: .section-lede reads
   --ub-blue-text, the slide switcher reads --proof-overlap and --switcher-gap,
   and the docking header below reads --ub-purple-deep and --ink. On any page
   that does not load home.css those references resolved to nothing, so the
   switcher sat at the wrong offset and a var()-only colour simply did not
   paint. Exactly the failure this file was created to stop: a component that
   can appear anywhere, depending on a sheet named after one page.

   Declared here as defaults, at the same values home.css uses. home.css still
   declares its own copies and loads later, so the homepage is unchanged and
   home.css stays a straight port of the mockup's; if the two ever disagree,
   that is a bug in whichever moved.
   -------------------------------------------------------------------------- */
:root {
  --ub-blue-text: #0A6E8F;   /* text-safe blue; fills keep the pure brand blue */
  --ub-purple-deep: #2A1240;
  --ink: #1B1023;
  --proof-overlap: 56px;
  --switcher-gap: 26px;
}


/* --------------------------------------------------------------------------
   Shared display typography
   Moved verbatim from home.css. Every UB section-block heading emits these
   three classes, so they must load wherever a block can be placed.
   -------------------------------------------------------------------------- */
/* Weight and tracking come from tokens; the SIZES stay literal. Size is this
   file's own design decision per class, and a single scale over two clamps
   with different floors, ceilings and viewport terms would not preserve the
   relationship between the hero and a section head — which is the whole point
   of the pair. The type-scale setting moves them together by way of the root
   font size instead, which is what rem is for.
   The tracking multiplier keeps each class's own value and scales it, so 1 is
   exactly -0.032em and -0.026em as before, and 0 removes the tightening. */
.v2 .display-xl {
  font-size: clamp(3.1rem, 6.6vw, 6.4rem);
  font-weight: var(--heading-weight);
  line-height: 0.98;
  letter-spacing: calc(-0.032em * var(--heading-tracking-scale));
}
.v2 .display-lg {
  font-size: clamp(2.3rem, 4vw, 3.6rem);
  font-weight: var(--heading-weight);
  line-height: 1.04;
  letter-spacing: calc(-0.026em * var(--heading-tracking-scale));
  color: var(--ub-purple);
}
.v2 .pg-labs:not(.pg-labs--light) .display-lg,
.v2 .pg-labs:not(.pg-labs--light) h2 {
  color: var(--white);
}
.v2 .pg-labs:not(.pg-labs--light) .eyebrow-rule {
  color: #7FD9F5;
}
.v2 .eyebrow-rule {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: var(--eyebrow-tracking);
  text-transform: uppercase;
  color: var(--ub-blue-text);
  margin-bottom: 22px;
}
.v2 .eyebrow-rule::before {
  content: '';
  width: 44px;
  height: 3px;
  background: var(--ub-blue);
}
.v2 .section-lede {
  font-size: 1.18rem;
  line-height: 1.65;
  color: var(--text-light);
  max-width: 46rem;
}

/* --------------------------------------------------------------------------
   Scroll-reveal primitive (JS adds .in-view)
   -------------------------------------------------------------------------- */
.v2 .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--reveal-dur) var(--transition-ease),
              transform var(--reveal-dur) var(--transition-ease);
}
.v2 .reveal.in-view { opacity: 1; transform: none; }

/* --------------------------------------------------------------------------
   Ghost button — standalone, so it works on any dark background.
   -------------------------------------------------------------------------- */
/* This restates .btn's radius at (0,2,0) so it beats style.css's (0,1,0) —
   which is why it has to read the token too. Left as a literal 0 it would pin
   every button on every `.v2` page square no matter what the settings said,
   and `.v2` is on almost every page.
   font-weight stays 700 rather than becoming --btn-weight (600): a ghost
   button sits on a dark ground where the lighter weight loses too much of the
   label. It is a deliberate difference from the filled variants, not a value
   that drifted. */
.v2 .btn { border-radius: var(--btn-radius); }
.v2 .btn-ghost {
  display: inline-block;
  background: transparent;
  color: var(--white);
  border: var(--btn-border-width) solid rgba(255,255,255,0.55);
  border-radius: var(--btn-radius);
  padding: calc(16px * var(--btn-pad-scale)) calc(32px * var(--btn-pad-scale));
  font-weight: 700;
  font-size: calc(1rem * var(--btn-font-scale));
  letter-spacing: var(--btn-tracking);
  text-transform: var(--btn-transform);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: var(--transition);
}
.v2 .btn-ghost:hover {
  background: var(--white);
  color: var(--ub-purple);
  border-color: var(--white);
  transform: translateY(calc(var(--btn-lift) * -1));
}

/* --------------------------------------------------------------------------
   Scroll progress hairline — SITE CHROME, every page.
   Unscoped on purpose: header.php emits this on every page and js/ub-chrome.js
   drives it on every page, so a `.v2` scope would silently disable it on any
   page that does not use section blocks. That three-way page-gating is the bug
   this move fixes.
   -------------------------------------------------------------------------- */
.scroll-progress {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 0%;
  background: linear-gradient(90deg, var(--ub-purple) 0%, var(--ub-blue) 100%);
  z-index: 9999;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   Sticky compact CTA (appears after the hero)
   -------------------------------------------------------------------------- */
.sticky-cta {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 900;
  background: rgba(27,16,35,0.96);
  backdrop-filter: blur(12px);
  transform: translateY(110%);
  transition: transform 0.45s cubic-bezier(0.25,0.8,0.25,1);
  border-top: 3px solid var(--ub-blue);
}
.sticky-cta.visible { transform: translateY(0); }
.sticky-cta .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 14px 20px;
}
.sticky-cta .msg {
  color: var(--white);
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: -0.01em;
}
.sticky-cta .msg span {
  display: block;
  font-weight: 400;
  font-size: 0.84rem;
  color: rgba(255,255,255,0.62);
  margin-top: 2px;
}
.sticky-cta .acts { display: flex; gap: 10px; flex-shrink: 0; }
.sticky-cta .btn { padding: 12px 24px; font-size: 0.92rem; }
.sticky-cta .dismiss {
  background: none;
  border: none;
  color: rgba(255,255,255,0.5);
  font-size: 1.1rem;
  cursor: pointer;
  padding: 6px 10px;
}
.sticky-cta .dismiss:hover { color: var(--white); }

@media (max-width: 768px) {
  .sticky-cta .container { flex-direction: column; align-items: stretch; }
  .sticky-cta .acts .btn { flex: 1; text-align: center; }
  .sticky-cta .msg span { display: none; }
}

/* ==========================================================================
   HERO CAROUSEL SLIDE SWITCHER — .hero2-switcher / .hs-*

   Travels with ub/hero-carousel: the block can be placed on any page, so its
   control cannot be styled from a page-named sheet.

   THE MOBILE REDESIGN (2026-08-03)
   The desktop control is a labelled row — four columns, each with an index, a
   name, and a 2px progress rule that doubles as the autoplay timer. That is
   the right control for a wide viewport and the wrong one for a phone, and it
   failed in two different ways at two different widths:

     769–1100px  Four labels shared a 3-column grid with minmax(0,1fr), so
                 they did not wrap — they COLLIDED. "Care for the City" ran
                 into "Built Here", and the fourth label clipped off-screen.
     ≤768px      grid-template-columns:1fr stacked all four full-width. It was
                 legible, but it read as a four-item text menu rather than a
                 carousel control, and it cost ~240px of vertical space inside
                 a hero — pushing the actual hero copy off a phone screen.

   Both are the same underlying mistake: a control whose information density is
   fixed to the label text, dropped into a viewport that cannot afford it.

   THE FIX — a numbered pill rail, the standard mobile carousel idiom:

     - One row of compact numbered pills (01 02 03 04), horizontal, ~56px of
       total height instead of ~240px.
     - Only the ACTIVE slide's label is shown, beneath the rail. The label is
       the useful part, so it is kept — but for one slide, not four at once, so
       there is nothing to collide.
     - Each pill is a >=44px touch target (WCAG 2.5.5 / Apple HIG minimum).
       The old stacked rows were ~40px and the squeezed columns ~30px wide.
     - The progress fill stays, as a ring around the active pill rather than a
       hairline, so the autoplay timer is still legible at arm's length.

   Accessibility is unchanged: this is CSS only. The markup keeps its
   role="tablist"/role="tab"/aria-selected from ub-section-blocks.php, and
   .hs-label is hidden from SIGHT with clip-path on inactive pills rather than
   display:none, so every slide name stays in the accessibility tree and
   screen-reader users still hear which slide each control selects.
   ========================================================================== */
.hero2-switcher {
  position: absolute;
  left: 0; right: 0;
  /* Sits clear of the stats bar overlapping the hero's bottom edge. */
  bottom: calc(var(--proof-overlap) + var(--switcher-gap));
  z-index: 6;
}
.hs-track {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  max-width: 39rem;
  margin-left: 35px; /* clears the hero2-inner border + padding */
}
.hs-item {
  position: relative;
  background: none;
  border: none;
  padding: 15px 20px 0 0;
  margin: 0;
  text-align: left;
  cursor: pointer;
  font-family: inherit;
  -webkit-appearance: none;
  appearance: none;
}
/* Base track */
.hs-item::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 16px;
  height: 2px;
  background: rgba(255,255,255,0.26);
}
/* Progress fill — doubles as the autoplay timer */
.hs-item::after {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 0;
  height: 2px;
  background: var(--ub-blue);
}
.hs-item.active::after {
  animation: hsProgress 7500ms linear forwards;
}
@keyframes hsProgress {
  from { width: 0; }
  to   { width: calc(100% - 16px); }
}
/* Pause the indicator in step with the carousel timer. */
.hero2:hover .hs-item.active::after { animation-play-state: paused; }

/* HIDDEN ABOVE 1100px, kept in the markup, and both halves of that matter.
   Hidden: position-in-sequence was never what the numeral was carrying. Each
   control's top hairline fills left to right as the autoplay timer runs
   (.hs-item::after below), so the fill already says which slide is showing AND
   how long is left — the numeral restated the weaker half of that in a second
   typographic layer, four times, directly under a hero whose whole problem was
   layer count. Removing it takes 22px off every control and off the reserve
   .hero2-body computes from --switcher-h.

   Kept in the markup: below 1100px the switcher is a pill rail, and the numeral
   IS the pill's label — only the active slide's name is shown, underneath. Strip
   the span from ub-section-blocks.php and mobile gets five empty 44px boxes. So
   this is display:none at desktop and display:block on the rail, not a template
   change. Screen readers are unaffected either way: the control's accessible
   name comes from .hs-label, which stays in the tree at both sizes. */
.hs-num {
  display: none;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  color: rgba(255,255,255,0.48);
  transition: var(--transition);
}
.hs-label {
  display: block;
  /* Was 5px, to sit under the numeral. There is nothing above it now. */
  margin-top: 0;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: -0.012em;
  line-height: 1.25;
  color: rgba(255,255,255,0.7);
  transition: var(--transition);
}
.hs-item:hover .hs-num,
.hs-item:focus-visible .hs-num { color: rgba(255,255,255,0.8); }
.hs-item:hover .hs-label,
.hs-item:focus-visible .hs-label { color: var(--white); }
.hs-item.active .hs-num { color: var(--ub-blue); }
.hs-item.active .hs-label { color: var(--white); }
.hs-item:focus-visible { outline: 2px solid var(--ub-blue); outline-offset: 3px; }

/* There used to be a `.hs-label { font-size: 0.88rem }` here, gated on
   min-width 1101px and max-height 900px, so the switcher gave up a pixel or two
   of label to a fold that was short of room. Hiding .hs-num above handed back
   22px per control — far more than that rule ever recovered — so the label now
   keeps one size at every desktop height, and this is one fewer height gate to
   keep in step with home.css. */

/* --------------------------------------------------------------------------
   The pill rail. Applies below 1100px — i.e. to BOTH previously-broken
   ranges, since the 769–1100px collision and the ≤768px stack are the same
   problem at different severities.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .hero2-switcher {
    position: relative;
    bottom: auto;
    padding: 0 0 40px;
  }

  /* auto-flow instead of a fixed column count: the rail sizes to the number of
     slides an editor actually added, so a 3- or 5-slide carousel is laid out
     correctly without a matching media query. The old fixed repeat(3) is what
     made a 4-slide carousel collide. */
  .hs-track {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    max-width: none;
    margin-left: 0;
    /* Reserves the active label's line so the rail does not jump 20px taller
       when a slide with a longer label becomes active. */
    padding-bottom: 1.6rem;
    position: relative;
  }

  .hs-item {
    /* >=44px in both directions: WCAG 2.5.5 Target Size (AAA) and the Apple
       HIG minimum. The old squeezed columns were ~30px wide. */
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 6px;
    border: 2px solid rgba(255,255,255,0.28);
    background: rgba(255,255,255,0.06);
    text-align: center;
    transition: background 0.25s ease, border-color 0.25s ease;
    /* The base rule sets position:relative so the hairline track can be
       absolutely placed against each item. On the rail that hairline is gone,
       and relative positioning here would make .hs-label resolve against the
       44px pill instead of the track — which truncates the label to "Our…"
       inside the pill rather than showing it underneath. static hands the
       containing block back to .hs-track. */
    position: static;
  }
  .hs-item:hover,
  .hs-item:focus-visible {
    background: rgba(255,255,255,0.14);
    border-color: rgba(255,255,255,0.5);
  }
  .hs-item.active {
    background: var(--ub-blue);
    border-color: var(--ub-blue);
  }

  /* The hairline track and its fill are a wide-viewport device. On the rail the
     active state is carried by the pill's own fill, so both are switched off
     rather than left to draw stray 2px marks across the pills. */
  .hs-item::before,
  .hs-item::after { display: none; }

  /* display:block restores what the base rule now switches off. On the rail the
     numeral is the pill's visible content, not a second layer above a label. */
  .hs-num {
    display: block;
    font-size: 0.82rem;
    letter-spacing: 0.06em;
    color: rgba(255,255,255,0.75);
  }
  .hs-item.active .hs-num { color: var(--white); }

  /* Only the ACTIVE slide's label is visible, positioned under the rail.
     clip-path rather than display:none keeps every label in the accessibility
     tree, so a screen reader still announces "Purple Knights" for pill 04
     while sighted users see one label at a time and nothing collides. */
  .hs-label {
    position: absolute;
    left: 0;
    bottom: 0;
    margin-top: 0;
    max-width: 100%;
    font-size: 0.95rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    clip-path: inset(50%);
    color: rgba(255,255,255,0.001);
  }
  .hs-item.active .hs-label {
    clip-path: none;
    color: var(--white);
  }
}

@media (max-width: 768px) {
  .hs-track { gap: 8px; }
  .hs-item { min-width: 46px; }
  .hs-num { font-size: 0.86rem; }
}

/* ==========================================================================
   DOCKING HEADER — .site-header.is-docked / .slim-brand / .slim-actions

   SITE CHROME, every page. Unscoped for the same reason .scroll-progress is:
   header.php emits this header everywhere and js/ub-chrome.js drives it
   everywhere, so gating the styles on a page class would reintroduce exactly
   the bug this file was created to fix.

   ---------------------------------------------------------------------------
   THE PROBLEM

   .site-header is `position: sticky; top: 0` in style.css, so all four tiers
   follow the reader down every page. Measured as rendered at 1366x768:

     centennial ribbon   29px
     utility bar         37px
     brand bar           83px
     nav bar             58px
     ------------------------
     held for the whole scroll:  207px, 27% of the viewport

   Two of those four tiers are directory furniture. Directory / Calendar /
   Library / Alumni / Parents / Give / Login, and the centennial ribbon, are
   things a reader consults once, at the top, on purpose. The logo, the menu and
   the conversion buttons are the ones worth carrying down a 9,000px page.

   THE FIX: ONE HEADER, TWO STATES

   The header lifts as the reader scrolls and stops lifting when only the nav
   row is left, docking as a single 58px bar. 207px → 58px returns 149px of
   window on every page of the site.

   THE LIFT IS SCROLL-LINKED, NOT ANIMATED

   ub-chrome.js writes `transform: translateY(-Npx)` where N is the scroll
   offset clamped to (header height − bar height). Below the clamp the header
   moves exactly as far as the page does, so it reads as an ordinary header
   scrolling away; at the clamp it stops and stays.

   That is the whole reason there is no seam. A threshold that swaps a 207px
   header for a 58px bar in one step has to answer "what fills the 149px the
   header just vacated?", and at any scroll offset below the clamp the answer is
   a sliver of background above the content, in view for the length of the
   animation, on the way back up. Deriving the lift from the scroll makes the
   header's bottom edge coincide with the top of the content by construction, at
   every offset, in both directions — no threshold to flicker across and no
   hysteresis to tune.

   transform is deliberate too: it does not participate in layout, so the
   header's flow box stays 207px throughout and the page never shifts by a
   pixel. The mega panels are already anchored to the nav row (`top: 100%` of
   .nav-container), so they keep dropping from the bar's bottom edge with no
   repositioning at all.

   WHAT --slim-h HOLDS, AND WHY IT IS MEASURED

   The docked bar's height must equal the nav row's own resting height. If it
   did not, the lift clamp — header height minus bar height — would be computed
   from one number while the layout settled at another, and the difference would
   show up as a gap under the bar. ub-chrome.js measures the resting row and
   writes it here as --slim-h; the docked row is pinned to it. It is measured
   rather than declared because the row is not a constant: the centennial ribbon
   is switchable in the Customizer, the menu is editable, and Outfit arrives over
   the network at a different height from the fallback stack.

   OTHER STICKY BARS

   ub-chrome.js publishes the same number as --ub-header-h (and the three names
   academics.css, program.css and brand-guide-wp.css already consume), so the
   academics search bar, the program jump bar and the brand guide's section nav
   pin directly under the docked bar instead of 149px below it.

   WHY THE DOCKED BAR IS PURPLE

   A white 58px strip reads as the same header, shrunk, and invites the reader to
   wonder what was taken away. --ub-purple-deep is the hero's own ground: it
   reads as a different and deliberate object, and it is the one colour on this
   page that cannot be lost against the white sections it floats over. The white
   lockup it needs is the file the footer already loads. Nothing was drawn, and
   no colour invented, for this state.

   DESKTOP ONLY (>1100px)

   Below 1100px style.css already hides the utility bar and the desktop menu,
   the header is two tiers, and the menu is a drawer — nothing to reclaim and
   nothing to dock. Everything here is inside the desktop query, and the mobile
   query at the end asserts the docked state cannot leak in.
   ========================================================================== */

.site-header {
  /* Overwritten by ub-chrome.js with the nav row's measured resting height.
     The fallback is what it measures at 1366x768, so a JS failure degrades to
     the plain sticky header rather than to a broken bar. */
  --slim-h: 58px;

  /* Docked-bar type and spacing, re-declared per fit tier below. These are the
     "mid" values, and they are what applies before ub-chrome.js has classified
     the available width. */
  --slim-nav-gap: 20px;
  --slim-nav-size: 0.9rem;
  --slim-logo-h: 34px;
  --slim-edge-gap: 26px;

  --slim-ease: cubic-bezier(0.22, 0.86, 0.28, 1);
  --slim-dur: 260ms;
}

/* Hidden at every width until the bar docks — out of the tab order, out of the
   accessibility tree, and out of the way of the resting centred nav row. */
.slim-brand,
.slim-actions {
  display: none;
}


@media (min-width: 1101px) {

  /* ---- 1. The lift ------------------------------------------------------
     ub-chrome.js owns .site-header's transform and nothing else does, so there
     is no transition on it: a transition would make the header lag the scroll
     it is locked to. */
  .site-header {
    will-change: transform;
  }

  /* style.css gives the nav links `transition: var(--transition)`, which is
     `all 0.3s`. Docking changes three of the properties `all` covers — height,
     padding and font-size — so on the way back out the links would animate
     their own geometry over 300ms. The row's height is its links' height, and
     the header's height is the row's, so that transition drags 27px of page
     content up and down behind it: a measured content jump produced entirely by
     a transition nobody wrote on purpose.

     Narrowed to colour, which is the only thing style.css actually animates on
     these links (the chevron rotates on its own transform transition). The
     resting header behaves identically; the docked one no longer moves the page. */
  .site-header .main-nav > ul > li > a {
    transition: color 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  }
  .site-header .main-nav > ul > li > a i {
    transition: transform 0.3s ease, color 0.3s ease;
  }

  /* The tiers that leave. Hidden from the tab order and from screen readers the
     moment the bar docks, because they are then translated above the top of the
     window where a focus ring cannot be scrolled into view: `visibility: hidden`
     is what stops a keyboard user being sent to a control they cannot see.
     Delayed by the colour transition so they do not blink out while the bar is
     still taking on its purple; on the way back the delay goes with the class,
     so they return in the same frame. */
  .site-header.is-docked .centennial-ribbon,
  .site-header.is-docked .utility-bar,
  .site-header.is-docked .brand-bar {
    visibility: hidden;
    transition: visibility 0s linear var(--slim-dur);
  }


  /* ---- 2. The bar -------------------------------------------------------
     The nav row is the bar, pinned to the height ub-chrome.js measured. */
  .nav-bar {
    transition: background-color var(--slim-dur) var(--slim-ease),
                box-shadow var(--slim-dur) var(--slim-ease),
                border-color var(--slim-dur) var(--slim-ease);
  }

  .site-header.is-docked .nav-bar {
    height: var(--slim-h);
    background: var(--ub-purple-deep);
    /* The 1px border is kept rather than removed: dropping it would change the
       row's height and therefore the lift clamp. */
    border-top-color: rgba(255, 255, 255, 0.10);
    box-shadow: 0 16px 34px -16px rgba(26, 10, 50, 0.62);
  }

  .site-header.is-docked .nav-container {
    height: 100%;
    align-items: center;
    gap: var(--slim-edge-gap);
  }

  /* The menu keeps `justify-content: center` from style.css, so it stays
     centred in whatever room the wordmark and buttons leave rather than jamming
     against the wordmark. */
  .site-header.is-docked .main-nav {
    flex: 1 1 auto;
    min-width: 0;
  }


  /* ---- 3. The wordmark and the buttons ----------------------------------
     The brand bar's own logo and buttons go visibility:hidden in the same
     moment these appear, so exactly one home link and one button set is live in
     either state. */
  .site-header.is-docked .slim-brand,
  .site-header.is-docked .slim-actions {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    /* One gesture, both clusters, no stagger: the bar arrives and its contents
       settle into it.

       No fill mode, deliberately. `both` would hold slim-settle's `from` frame
       through the 60ms delay, making opacity:0 the resting state of the docked
       wordmark and buttons — so a browser that never advanced the animation
       would dock an empty purple bar. That exact failure hit .ad-pane on
       /admissions/ (see CHANGELOG). Without a fill mode the delay shows the
       real element and the animation only adds the settle on top. */
    animation: slim-settle var(--slim-dur) var(--slim-ease) 60ms;
  }

  .site-header.is-docked .slim-actions {
    gap: 10px;
  }

  .slim-brand {
    opacity: 0.94;
    transition: opacity var(--slim-dur) var(--slim-ease);
  }
  .slim-brand:hover {
    opacity: 1;
  }
  .slim-brand img {
    display: block;
    height: var(--slim-logo-h);
    width: auto;
  }

  @keyframes slim-settle {
    from { opacity: 0; transform: translateY(-5px); }
    to   { opacity: 1; transform: none; }
  }


  /* ---- 4. The menu on a purple ground -----------------------------------
     style.css paints the nav links --text-main and their hover --ub-purple,
     both of which vanish on --ub-purple-deep. */
  .site-header.is-docked .main-nav > ul {
    gap: var(--slim-nav-gap);
  }

  /* The full height of the row, less the 1px border it keeps, so the hit area
     is the whole bar and the row cannot overflow its own pinned height.
     style.css's 15px of vertical padding is what sets the row's height when it
     is NOT docked, which is the number --slim-h holds — so here the padding
     goes and the height is stated directly. */
  .site-header.is-docked .main-nav > ul > li > a {
    position: relative;
    height: calc(var(--slim-h) - 1px);
    padding-top: 0;
    padding-bottom: 0;
    font-size: var(--slim-nav-size);
    font-weight: 600;
    color: rgba(255, 255, 255, 0.90);
    white-space: nowrap;
  }

  .site-header.is-docked .main-nav > ul > li > a i {
    color: rgba(255, 255, 255, 0.55);
  }

  .site-header.is-docked .main-nav > ul > li:hover > a,
  .site-header.is-docked .main-nav > ul > li > a:focus-visible {
    color: var(--white);
  }
  .site-header.is-docked .main-nav > ul > li:hover > a i {
    color: var(--white);
  }

  /* Underlines the label, not the bar: at the row's bottom edge it would touch
     the 3px rule on top of the mega panel it opens. */
  .site-header.is-docked .main-nav > ul > li:not(.mega-menu-trigger--search) > a::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 15px;
    height: 2px;
    background: var(--ub-blue);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 240ms var(--slim-ease);
  }
  .site-header.is-docked .main-nav > ul > li:hover > a::after,
  .site-header.is-docked .main-nav > ul > li > a:focus-visible::after {
    transform: scaleX(1);
  }

  /* The quick-links panel is pinned to `right: 0` of .nav-container. Unstuck
     that is roughly under the magnifier; docked, the buttons hold that edge and
     the panel would open some 300px away from its own trigger. Anchoring it to
     the trigger fixes that, and the trigger is exactly one row tall so
     `top: 100%` still lands on the bar's bottom edge. */
  .site-header.is-docked .mega-menu-trigger--search {
    position: relative;
  }
  .site-header.is-docked .mega-menu--narrow {
    left: auto;
    right: 0;
  }


  /* ---- 5. The buttons on a purple ground --------------------------------
     The same header-actions menu, restyled for the ground it now sits on. The
     inversion is the one style.css already defines for dark surfaces —
     .btn-outline-light's resting state and its hover are exactly this pairing —
     so no new button exists. */
  .site-header.is-docked .slim-actions .btn {
    padding: 9px 18px;
    font-size: 0.85rem;
    font-weight: 700;
    line-height: 1.2;
    border-width: 1px;
    border-style: solid;
    white-space: nowrap;
  }

  /* Request Info does not fit, at any width. .container caps at 1280px, so the
     bar has 1240px however wide the screen gets, and the row measures
     wordmark 128 + menu 637 + gaps 156 + edges 52 + three buttons 345 = 1318.
     The only thing left to take 78px out of is the menu — 16px gaps at 14px
     type, which turns six top-level destinations into a run-on list.

     The outlined button is the one that goes because it is the only one whose
     destination is also inside the bar: Admissions & Aid → Request Information,
     one hover away in the menu that is right there. It stays in the
     header-actions menu and in the brand bar; it is this copy that is dropped. */
  .site-header.is-docked .slim-actions .btn-outline {
    display: none;
  }

  .site-header.is-docked .slim-actions .btn-primary {
    background: var(--white);
    border-color: var(--white);
    color: var(--ub-purple-deep);
  }
  .site-header.is-docked .slim-actions .btn-primary:hover {
    background: var(--ub-blue);
    border-color: var(--ub-blue);
    color: var(--ink);
    transform: translateY(-1px);
    box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.55);
  }

  /* .btn-ai is `border: none` in style.css, so the width above alone would
     render nothing. On deep purple the gradient's purple end is close enough to
     the bar that the button's left edge dissolves; the hairline is what keeps
     the silhouette. */
  .site-header.is-docked .slim-actions .btn-ai {
    border: 1px solid rgba(255, 255, 255, 0.24);
    box-shadow: none;
    gap: 7px;
  }
  .site-header.is-docked .slim-actions .btn-ai:hover {
    border-color: rgba(255, 255, 255, 0.5);
    box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.55);
  }
  .site-header.is-docked .slim-actions .btn-ai .ai-sparkle-icon {
    width: 16px;
    height: 16px;
  }

  /* White text on --ub-purple-deep needs its own focus ring; the purple one
     used elsewhere would be invisible here. Inset on the nav links, which run
     the full height of the bar and would clip an outset ring. */
  .site-header.is-docked .main-nav > ul > li > a:focus-visible {
    outline: 2px solid var(--ub-blue);
    outline-offset: -4px;
  }
  .site-header.is-docked .slim-actions .btn:focus-visible,
  .site-header.is-docked .slim-brand:focus-visible {
    outline: 2px solid var(--ub-blue);
    outline-offset: 3px;
  }


  /* ---- 6. Fit tiers -----------------------------------------------------
     Keyed off a data attribute rather than a media query, because the width the
     bar actually gets is neither the window's nor a fixed fraction of it:

       · .container caps at 1280px, so above a 1280px wrapper the bar's usable
         width stops growing at 1240px no matter how wide the screen is. A
         1920px query and a 1280px query describe the same bar.
       · the AI sidebar is a flex sibling that takes 400px out of
         .main-content-wrapper, so a 1440px window with the assistant open
         leaves the bar 1040px while every width query still reads 1440.

     ub-chrome.js measures the wrapper and classifies it. Each tier was set by
     measuring the row's real content, not by picking round numbers:

       wide   ≥1280px   1240px of bar; 128 logo + 637 menu + 156 gaps + 219
                        buttons + 52 edges = 1192, and 48px spare.
       mid    1180–1279  the container starts shrinking with the wrapper.
       tight  1080–1179  0.85rem is where the seven items still clear the
                        buttons at a 1101px window, the narrowest desktop.
       micro  970–1079   Ask AI drops to its icon and keeps its aria-label.
       min    <970px     the wordmark goes too.

     The last two are only reachable with the assistant open: without it the
     wrapper is the window, and the window is at least 1101px here.

     Below roughly 864px of wrapper the menu does not fit any single row at any
     type size. The four-tier header crosses that line first and harder — its
     menu is 1.05rem with 40px gaps, and at 966px of wrapper the labels start
     wrapping inside their own links, taking the row to 85px and the header to
     234px. The docked bar tracks whatever height that row settles at, which is
     why it stays seamless there. */
  .site-header[data-slim-fit="wide"] {
    --slim-nav-gap: 26px;
    --slim-nav-size: 0.95rem;
    --slim-logo-h: 38px;
    --slim-edge-gap: 30px;
  }
  .site-header[data-slim-fit="mid"] {
    --slim-nav-gap: 20px;
    --slim-nav-size: 0.9rem;
    --slim-logo-h: 34px;
    --slim-edge-gap: 26px;
  }
  .site-header[data-slim-fit="tight"] {
    --slim-nav-gap: 16px;
    --slim-nav-size: 0.85rem;
    --slim-logo-h: 30px;
    --slim-edge-gap: 20px;
  }
  .site-header[data-slim-fit="micro"],
  .site-header[data-slim-fit="min"] {
    --slim-nav-gap: 13px;
    --slim-nav-size: 0.85rem;
    --slim-logo-h: 28px;
    --slim-edge-gap: 16px;
  }

  /* The label goes, the button keeps its aria-label. The walker prints the
     label as a bare text node, so there is no element to hide — font-size: 0
     collapses it, and the icon is sized in px so it is unaffected. gap goes
     with it, or the zero-width text node still costs 7px. */
  .site-header[data-slim-fit="micro"] .slim-actions .btn-ai,
  .site-header[data-slim-fit="min"] .slim-actions .btn-ai {
    padding: 9px 11px;
    font-size: 0;
    gap: 0;
  }

  .site-header[data-slim-fit="min"].is-docked .slim-brand {
    display: none;
  }


  /* ---- 7. The bottom apply bar, which the docked bar makes redundant ----
     ub-chrome.js reveals #stickyCta once the reader is past the hero: a purple
     bar across the bottom of the window carrying Apply Now and Request Info.
     That bar existed because Apply Now scrolled away with the brand bar and
     nothing replaced it.

     Docked, it stops being a reminder and becomes a duplicate — the same
     purple, the same Apply Now, with the reader's content in the sandwich. It
     is also 83px of the bottom of the window, which is most of what a slimmer
     header just handed back.

     Hidden while the header is docked, which is every offset at which
     ub-chrome.js would have shown it. Keyed on the body class because
     #stickyCta lives outside .page-layout and is not the header's sibling.
     Deleting these three lines gives it back. */
  body.header-docked .sticky-cta {
    display: none;
  }


  /* ---- 8. The progress hairline -----------------------------------------
     3px of purple-to-blue, fixed at the top of the window at z-index 9999, so
     it lands on the docked bar. Its purple half is invisible on
     --ub-purple-deep; on the bar it runs blue to the ribbon's own light blue
     instead, which turns a lost element into the bar's progress line. */
  body.header-docked .scroll-progress {
    background: linear-gradient(90deg, var(--ub-blue) 0%, #7FD9F5 100%);
  }
}


/* --------------------------------------------------------------------------
   MOBILE — assert the docked state cannot leak in.

   ub-chrome.js drops the class and clears the transform below 1101px, but if a
   resize ever lands between those two things, these rules are what keep the
   mobile header from rendering as half a docked bar.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .site-header {
    transform: none !important;
    will-change: auto;
  }
  .slim-brand,
  .slim-actions {
    display: none;
  }
}


/* --------------------------------------------------------------------------
   Reduced motion. Kept with the components it silences rather than in a
   page sheet, for the same reason as everything else in this file.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .v2 .reveal { opacity: 1; transform: none; transition: none; }
  .v2 .hero2-image { animation: none !important; }
  /* Show the switcher state without the timer sweep. */
  .hs-item.active::after { animation: none; width: calc(100% - 16px); }
  .sticky-cta { transition: none; }

  /* The lift stays: it is not an animation, it is the header's position at a
     given scroll offset, and removing it would leave the four-tier header over
     the content this exists to give back. What goes is the settle and the
     underline sweep. */
  .site-header.is-docked .slim-brand,
  .site-header.is-docked .slim-actions { animation: none; }
  .site-header.is-docked .main-nav > ul > li > a::after { transition: none; }
  .site-header.is-docked .slim-actions .btn:hover { transform: none; }
}
