/* ==========================================================================
   Unemployable Property — site components
   Copied from shared/components.css, swept token-clean, then extended with
   the SITE KIT block at the foot of the file.
   Every page links this AFTER /assets/css/tokens.css.
   Rules: no raw hex, no rgba() literals, breakpoints 640 / 768 / 1024 only.
   ========================================================================== */

/* -------- Layout containers -------- */
.uep-container {
  width: 100%;
  max-width: var(--max-w-xwide);
  margin-inline: auto;
  padding-inline: var(--space-5);
}
.uep-container--narrow { max-width: var(--max-w-narrow); }
.uep-container--body   { max-width: var(--max-w-body);   }
.uep-container--wide   { max-width: var(--max-w-wide);   }
/* Edge-to-edge. Keeps the container's side padding so nothing touches the
   viewport edge, but drops the max-width so a grid can use the whole screen.
   Only worth reaching for when the content is a grid of equal tiles — running
   body copy at 1900px is unreadable, which is what --wide exists to prevent. */
.uep-container--full { max-width: none; }

@media (min-width: 768px) {
  .uep-container { padding-inline: var(--space-6); }
}

/* -------- Section -------- */
.uep-section {
  padding-block: var(--space-8);
}
.uep-section--tight  { padding-block: var(--space-7); }
.uep-section--loose  { padding-block: var(--space-10); }
.uep-section--dark {
  background: var(--uep-navy-deep);
  color: var(--uep-on-dark);
}
.uep-section--dark h1,
.uep-section--dark h2,
.uep-section--dark h3,
.uep-section--dark h4,
.uep-section--dark h5,
.uep-section--dark h6 { color: var(--uep-on-dark); }
.uep-section--ink {
  background: var(--uep-ink);
  color: var(--uep-on-dark);
}
.uep-section--ink h1,
.uep-section--ink h2,
.uep-section--ink h3,
.uep-section--ink h4,
.uep-section--ink h5,
.uep-section--ink h6 { color: var(--uep-on-dark); }
.uep-section--green {
  background: var(--uep-green);
  color: var(--uep-on-dark);
}
.uep-section--green h1,
.uep-section--green h2,
.uep-section--green h3 { color: var(--uep-on-dark); }
.uep-section--warm { background: var(--uep-surface-warm); }
.uep-section--alt  { background: var(--uep-surface-alt);  }

@media (min-width: 768px) {
  .uep-section        { padding-block: var(--space-9); }
  .uep-section--tight { padding-block: var(--space-8); }
}

/* -------- Eyebrow / overline -------- */
.uep-overline {
  display: inline-block;
  font-family: var(--font-headline);
  font-size: var(--fs-overline);
  font-weight: var(--fw-sbd);
  letter-spacing: var(--tracking-xwide);
  text-transform: uppercase;
  /* 12px semibold is normal text under WCAG, so it needs 4.5:1. --uep-green is
     3.87:1 on white; --uep-green-darker is 7.58:1 on white and 7.33:1 on alt. */
  color: var(--uep-green-darker);
  margin-bottom: var(--space-3);
}
/* --uep-gold-dark is 2.43:1 on white. The "gold" eyebrow variant on a light
   panel therefore renders in the deep green; gold is reserved for dark grounds. */
.uep-overline--gold { color: var(--uep-green-darker); }
.uep-overline--light { color: var(--uep-gold); }
.uep-overline::before {
  content: "";
  display: inline-block;
  width: 24px;
  height: 2px;
  background: currentColor;
  vertical-align: middle;
  margin-right: var(--space-3);
  transform: translateY(-2px);
}

/* -------- Display heading variants -------- */
.uep-display {
  font-size: var(--fs-display);
  font-weight: var(--fw-blk);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}
.uep-title-lg { font-size: var(--fs-h1); line-height: var(--lh-tight); }
.uep-title-md { font-size: var(--fs-h2); line-height: var(--lh-snug); }
.uep-title-sm { font-size: var(--fs-h3); line-height: var(--lh-snug); }

.uep-lead {
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--uep-slate);
  max-width: 60ch;
}

.uep-strike { color: var(--uep-gold); }

/* -------- Buttons -------- */
.uep-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-headline);
  font-weight: var(--fw-sbd);
  font-size: var(--fs-body);
  line-height: 1;
  letter-spacing: var(--tracking-slight);
  padding: var(--space-4) var(--space-6);
  min-height: 52px;
  /* DEFECT FIX: an inline-flex box sizes to max-content, so a long label did not
     wrap — it simply grew past its container and pushed the whole page into
     horizontal overflow. Measured on /case-studies/: a 43-character label
     rendered 488px wide inside a 308px column on a 371px screen.
     max-width caps the box at its container so the text wraps instead, and the
     leading gives the second line room; line-height:1 had the two lines
     touching. Single-line buttons are unaffected — min-height still governs
     their height at every size. */
  max-width: 100%;
  line-height: var(--lh-snug);
  border: 2px solid transparent;
  border-radius: var(--radius-pill);
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  /* nowrap keeps a two-word label on one line, which is right for almost every
     button here. It is lifted below 640 for long labels — see the media query
     under .uep-btn--xl: with nowrap AND max-width the text could not break, so
     the box capped at its container while the text overflowed and was clipped
     mid-word. Both have to give at the same breakpoint or neither works. */
  white-space: nowrap;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.uep-btn:focus-visible { outline-offset: 3px; }
.uep-btn:active { transform: translateY(1px); }

/* One green: the brand green, #069550. The hover state stays on
   --uep-green-darker so the state change is still legible. */
.uep-btn--primary {
  background: var(--uep-green);
  color: var(--uep-on-dark);
  box-shadow: var(--shadow-glow-green);
}
.uep-btn--primary:hover {
  background: var(--uep-green-darker);
  color: var(--uep-on-dark);
  box-shadow: var(--shadow-glow-green-hover);
  transform: translateY(-1px);
}

.uep-btn--gold {
  background: var(--uep-gold);
  color: var(--uep-ink);
  box-shadow: var(--shadow-glow-gold);
}
.uep-btn--gold:hover {
  background: var(--uep-gold-dark);
  color: var(--uep-ink);
  transform: translateY(-1px);
}

.uep-btn--ghost {
  background: transparent;
  color: var(--uep-ink);
  border-color: var(--uep-ink);
}
.uep-btn--ghost:hover {
  background: var(--uep-ink);
  color: var(--uep-on-dark);
}

.uep-btn--ghost-light {
  background: transparent;
  color: var(--uep-on-dark);
  border-color: color-mix(in srgb, var(--uep-on-dark) 70%, transparent);
}
.uep-btn--ghost-light:hover {
  background: var(--uep-surface);
  color: var(--uep-ink);
}

.uep-btn--block { width: 100%; }
.uep-btn--xl { padding: var(--space-5) var(--space-6); min-height: 60px; font-size: var(--fs-lead); }
/* The xl scale is tuned for desktop heroes. Below 640 its padding and type push
   a long label onto three lines, so it steps back to the base scale. */
@media (max-width: 639px) {
  .uep-btn--xl { padding: var(--space-4) var(--space-5); min-height: 52px; font-size: var(--fs-body); }
  /* Long labels wrap rather than overflow. Paired with the max-width on .uep-btn:
     the cap alone only moved the failure from "pushes the page sideways" to
     "clipped mid-word". */
  .uep-btn { white-space: normal; }
}

/* -------- Pill tag -------- */
.uep-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-headline);
  font-size: var(--fs-small);
  font-weight: var(--fw-sbd);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--uep-green-tint);
  color: var(--uep-green-darker);
}
/* Pill text is 14px semibold — normal text under WCAG, so 4.5:1. The gold
   variant used --uep-gold-dark, which is 2.43:1 on white and only 2.19:1 on
   its own 18% gold wash: the same 2.43:1 failure the overline variant is
   already corrected for eleven lines above. The wash carries the "gold" read;
   the label does not have to, so the label is ink (16.84:1 on the wash over
   white, 11.14:1 on solid gold if the wash is ever dropped). No page uses this
   modifier yet — which is precisely why it is worth correcting now rather than
   after someone reaches for it. */
.uep-pill--gold {
  background: color-mix(in srgb, var(--uep-gold) 18%, transparent);
  color: var(--uep-ink);
}
.uep-pill--dark {
  background: var(--uep-overlay-white-08);
  color: var(--uep-on-dark);
  border: 1px solid var(--uep-overlay-white-18);
}

/* -------- Cards -------- */
.uep-card {
  background: var(--uep-surface);
  border: 1px solid var(--uep-line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  transition: transform var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out),
              border-color var(--dur-med) var(--ease-out);
}
.uep-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
  border-color: transparent;
}
.uep-card--dark {
  background: var(--uep-navy);
  border-color: var(--uep-overlay-white-08);
  color: var(--uep-on-dark);
}
.uep-card--dark h3 { color: var(--uep-on-dark); }
.uep-card--flush { padding: 0; overflow: hidden; }

.uep-card__media { aspect-ratio: 16 / 10; overflow: hidden; }
.uep-card__media img,
.uep-card__media video { width: 100%; height: 100%; object-fit: cover; }
.uep-card__body { padding: var(--space-5) var(--space-6) var(--space-6); }

/* -------- Grid utilities -------- */
.uep-grid { display: grid; gap: var(--space-5); }
.uep-grid-2 { grid-template-columns: 1fr; }
.uep-grid-3 { grid-template-columns: 1fr; }
.uep-grid-4 { grid-template-columns: 1fr; }
@media (min-width: 768px) {
  .uep-grid-2 { grid-template-columns: repeat(2, 1fr); }
  .uep-grid-3 { grid-template-columns: repeat(2, 1fr); gap: var(--space-6); }
  .uep-grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .uep-grid-3 { grid-template-columns: repeat(3, 1fr); }
  .uep-grid-4 { grid-template-columns: repeat(4, 1fr); }
}

/* -------- Header -------- */
.uep-header {
  position: sticky;
  top: 0;
  z-index: 40;
  /* Flat and opaque. The frosted-glass treatment that used to live here
     (backdrop-filter: saturate(180%) blur(14px)) is banned by the design
     language, and it also forced a compositing layer on every page. */
  background: var(--uep-surface);
  border-bottom: 1px solid var(--uep-line);
  transition: box-shadow var(--dur-med) var(--ease-out);
}
/* DEFECT FIX: app.js toggles .is-scrolled but nothing styled it, so the
   sticky header never lifted off the content beneath it. */
.uep-header.is-scrolled { box-shadow: var(--shadow-md); }

.uep-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: var(--space-4);
  gap: var(--space-6);
}
.uep-logo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-headline);
  font-weight: var(--fw-blk);
  font-size: var(--fs-h5);
  letter-spacing: var(--tracking-tight);
  color: var(--uep-ink);
}
.uep-logo__img {
  display: block;
  height: 40px;
  width: auto;
  max-width: 220px;
  object-fit: contain;
}
/* Source logo is white-on-transparent; invert for light header bg. */
.uep-header .uep-logo__img { filter: brightness(0); }
.uep-footer .uep-logo__img { filter: none; }
@media (min-width: 768px) {
  .uep-logo__img { height: 44px; max-width: 240px; }
}
.uep-nav { display: none; }
@media (min-width: 1024px) {
  .uep-nav { display: flex; align-items: center; gap: var(--space-6); }
}
.uep-nav a {
  font-family: var(--font-headline);
  font-weight: var(--fw-med);
  color: var(--uep-ink);
  font-size: var(--fs-small);
}
.uep-nav a:hover { color: var(--uep-green-darker); }
.uep-nav a[aria-current="page"] { color: var(--uep-green-darker); font-weight: var(--fw-sbd); }

.uep-header__cta { display: none; }
@media (min-width: 768px) { .uep-header__cta { display: inline-flex; } }

.uep-menu-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  background: transparent; border: 1px solid var(--uep-line);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
@media (min-width: 1024px) { .uep-menu-btn { display: none; } }
.uep-menu-btn svg { width: 22px; height: 22px; }

.uep-mobile-nav {
  display: none;
  /* DEFECT FIX: this was `padding: var(--space-4) 0 var(--space-5)`. The element
     also carries .uep-container, which sets padding-inline: var(--space-5) — and
     the shorthand's `0` overwrote it, because both are single-class selectors and
     this rule is 330 lines later in the file. Every menu item sat hard against
     the left edge of the phone screen. padding-block touches only the axis this
     rule means to own, so the container keeps its gutters. */
  padding-block: var(--space-4) var(--space-5);
  border-top: 1px solid var(--uep-line);
}
.uep-mobile-nav.is-open { display: block; }
/* :not(.uep-btn) on both this and the green-header override below. The mobile
   nav ends with a Member Login BUTTON, which is an <a>, so a bare
   `.uep-mobile-nav a { color: ... }` repainted it — and on the green header that
   meant white text on the button's own white ground, i.e. an empty white pill.
   A button brings its own colours; the nav only owns its links. */
.uep-mobile-nav a:not(.uep-btn) {
  display: block;
  padding: var(--space-3) 0;
  color: var(--uep-ink);
  font-weight: var(--fw-med);
}
@media (min-width: 1024px) { .uep-mobile-nav.is-open { display: none; } }

/* -------- Green header variant -------- */
/* Brand-forward nav: solid green, white logo, white nav, gold accents.
   Flows into the green hero at the top; lifts on a soft shadow when stuck
   over lighter sections below. */
/* Ground is --uep-green, the brand green, matching the hero it flows into.
   Nav hover is gold AND underlined: the underline carries the state change for
   anyone who does not read the colour shift. */
.uep-header--green {
  background: var(--uep-green);
  border-bottom-color: transparent;
  box-shadow: var(--shadow-md);
}
.uep-header--green .uep-logo { color: var(--uep-on-dark); }
.uep-header--green .uep-logo__img { filter: none; }      /* source logo is white-on-transparent */
.uep-header--green .uep-nav a { color: var(--uep-on-dark); }
.uep-header--green .uep-nav a:hover {
  color: var(--uep-gold);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}
.uep-header--green .uep-nav a[aria-current="page"] {
  color: var(--uep-on-dark);
  text-decoration: underline;
  text-decoration-color: var(--uep-gold);
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}
.uep-header--green .uep-menu-btn {
  border-color: var(--uep-overlay-white-18);
  color: var(--uep-on-dark);
}
.uep-header--green .uep-mobile-nav { border-top-color: var(--uep-overlay-white-18); }
.uep-header--green .uep-mobile-nav a:not(.uep-btn) { color: var(--uep-on-dark); }

/* -------- Footer -------- */
.uep-footer {
  background: var(--uep-navy-deep);
  color: var(--uep-on-dark-muted);
  padding-block: var(--space-8) var(--space-6);
}
.uep-footer a { color: var(--uep-on-dark); }
.uep-footer a:hover { color: var(--uep-gold); }
.uep-footer__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  padding-bottom: var(--space-7);
  border-bottom: 1px solid var(--uep-overlay-white-08);
}
@media (min-width: 768px) { .uep-footer__grid { grid-template-columns: 1.4fr 1fr 1fr 1fr; } }
/* A `1fr` track floors at min-content. The contact email has no break
   opportunity, so at 768px it grabbed 263px — 37% of the footer — and squeezed
   the 1.4fr brand column down to 142px, inverting the declared ratio and
   overflowing the page at a 20px root size. min-width: 0 releases the floor;
   overflow-wrap lets the address break inside its own track. */
.uep-footer__grid > * { min-width: 0; }
.uep-footer__list a { overflow-wrap: anywhere; }
/* The brand column and its blurb were carrying classes with no rule behind
   them. These two restate what the column was already inheriting and pin the
   measure, so the hook and the styling live in the same place. */
.uep-footer__brand > :last-child { margin-bottom: 0; }
.uep-footer__blurb { max-width: 46ch; color: var(--uep-on-dark-muted); }
.uep-footer__logo { color: var(--uep-on-dark); }
.uep-footer__title {
  color: var(--uep-on-dark);
  font-family: var(--font-headline);
  font-size: var(--fs-small);
  font-weight: var(--fw-sbd);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  margin: 0 0 var(--space-4);
}
.uep-footer__list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-3); }
.uep-footer__legal {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-3);
  padding-top: var(--space-5);
  font-size: var(--fs-small); color: var(--uep-on-dark-faint);
}
.uep-footer__legal a { color: var(--uep-on-dark-muted); }

/* -------- Form -------- */
.uep-form { display: grid; gap: var(--space-4); }
.uep-field { display: grid; gap: var(--space-2); }
.uep-field > label {
  font-family: var(--font-headline);
  font-size: var(--fs-small);
  font-weight: var(--fw-sbd);
  letter-spacing: var(--tracking-slight);
  color: var(--uep-ink);
}
.uep-field > input,
.uep-field > select,
.uep-field > textarea {
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  color: var(--uep-ink);
  background: var(--uep-surface);
  border: 1.5px solid var(--uep-line);
  border-radius: var(--radius-md);
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.uep-field > textarea { min-height: 130px; resize: vertical; }
/* No `outline: none` here — it out-specifies the global :focus-visible ring and
   left keyboard users with only a 3.87:1 green border to go on. */
.uep-field input:focus,
.uep-field select:focus,
.uep-field textarea:focus {
  border-color: var(--uep-green-darker);
  box-shadow: var(--shadow-focus-green);
}
.uep-field .uep-hint {
  font-size: var(--fs-small);
  color: var(--uep-slate);
}

/* -- Required and error states --------------------------------------------
   Both have to survive being read rather than seen, which is why the markup
   carries the requirement and this block only draws it.

   REQUIRED. The `required` attribute is the non-visual half and it was already
   there; nothing showed it on screen, so a sighted visitor met the requirement
   for the first time as a browser error bubble after pressing the button.
   .uep-field__req is the visible half. It is REAL TEXT inside the <label>, not
   a ::after asterisk: generated content is not reliably announced, is not
   selectable, and "asterisk" is not a word for "required". Because it lives in
   the label it is part of the field's accessible name, so both halves say the
   same thing and neither can drift from the other.

   ERROR. :user-invalid, not :invalid — :invalid matches an empty required
   field on first paint, so the form would open with every box already scolding
   someone who has not typed anything yet. :user-invalid waits until the field
   has been edited or submitted. The state is carried by BOTH the border colour
   (--uep-danger, 5.31:1 on white, and 4.29:1 against --uep-line so the change
   itself is perceivable) and a thicker ring, so it is not colour alone
   (WCAG 1.4.1). The browser's own validation message is the text half; there
   is no script on these forms to write a custom one, and inventing a silent
   visual-only error state would be strictly worse than the native bubble. */
.uep-field__req {
  font-weight: var(--fw-med);
  color: var(--uep-slate);
}
.uep-field > input:user-invalid,
.uep-field > select:user-invalid,
.uep-field > textarea:user-invalid {
  border-color: var(--uep-danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--uep-danger) 14%, transparent);
}
.uep-field > input:user-invalid:focus,
.uep-field > select:user-invalid:focus,
.uep-field > textarea:user-invalid:focus {
  border-color: var(--uep-danger);
}
.uep-field--radio-group,
.uep-field--check-group { gap: var(--space-3); }
.uep-radio, .uep-check {
  display: flex; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1.5px solid var(--uep-line);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.uep-radio input, .uep-check input { accent-color: var(--uep-green); margin-top: var(--space-1); }
.uep-radio:hover, .uep-check:hover { border-color: var(--uep-green); background: var(--uep-green-tint); }
.uep-radio:has(input:checked),
.uep-check:has(input:checked) {
  border-color: var(--uep-green);
  background: var(--uep-green-tint);
}

.uep-form__actions { margin-top: var(--space-3); }
.uep-form__legal {
  font-size: var(--fs-small);
  color: var(--uep-slate);
}

/* -------- Media frames -------- */
.uep-video {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--uep-ink);
  box-shadow: var(--shadow-lg);
}
.uep-video video,
.uep-video iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
  /* object-fit: cover, not the default contain. The box is aspect-ratio 16/9
     on a fluid width, so its computed height is fractional — 571.5px wide is
     321.47px tall — and a 16:9 poster laid out with contain cannot land on
     whole pixels inside it. The remainder shows as ink hairlines along two
     edges, which read as a black border around the video.

     Nothing is cropped in practice: every source this component carries is
     itself 16:9, so cover only absorbs the sub-pixel remainder. A non-16:9
     source WOULD be cropped here rather than letterboxed — give it its own
     modifier rather than reverting this. */
  object-fit: cover;
  display: block;
}
/* DEFECT FIX: a src-less iframe is NOT transparent. Chrome loads about:blank
   into it and paints an OPAQUE canvas, so on every facade — poster underneath,
   empty iframe on top waiting for its data-src — the poster was covered and the
   frame read as a solid black box until the visitor clicked. Keep the iframe out
   of the paint until it actually has a source. initVideoPlay() sets src on
   click, which flips this selector on its own; no JS change needed.
   visibility, not display, so the element keeps its box and nothing reflows. */
.uep-video iframe:not([src]) { visibility: hidden; }
/* The still that sits under a facade's iframe until someone presses play. Both
   fill the 16:9 box .uep-video gives them; the iframe is held at
   visibility:hidden above until it has a src, so this is what shows first.
   (.uep-story__poster on the homepage is an identical private copy that
   predates this and could be folded in.) */
.uep-video__poster {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.uep-video__play {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background: var(--uep-overlay-ink-35);
  color: var(--uep-on-dark);
  cursor: pointer;
  transition: background var(--dur-med) var(--ease-out);
}
.uep-video__play:hover { background: var(--uep-overlay-ink-55); }
.uep-video__play-icon {
  width: 84px; height: 84px; border-radius: var(--radius-pill);
  background: var(--uep-gold);
  color: var(--uep-ink);
  display: grid; place-items: center;
  box-shadow: var(--shadow-glow-ink-deep);
  transition: transform var(--dur-med) var(--ease-out);
}
.uep-video__play:hover .uep-video__play-icon { transform: scale(1.06); }

/* -------- Hero splits -------- */
.uep-hero {
  position: relative;
  padding-block: var(--space-9) var(--space-8);
  background: var(--uep-navy-deep);
  color: var(--uep-on-dark);
  overflow: hidden;
}
/* The overlay box only. Every hero in the tree is .uep-hero--green, which
   supplies the one signature gradient; a second, unspecified navy treatment
   here would be dead code defining a look nobody asked for. */
.uep-hero::before {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
}
.uep-hero__grid {
  position: relative;
  display: grid;
  gap: var(--space-7);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 1024px) {
  .uep-hero__grid { grid-template-columns: 1.15fr 1fr; gap: var(--space-8); }
}
.uep-hero h1 { color: var(--uep-on-dark); }
.uep-hero__kicker { color: var(--uep-gold); }

/* Signature green hero — green as primary.
   Solid brand green (#069550) with the white-highlight / ink-vignette overlay.
   Same green as .uep-header--green above and .uep-section--green below, so the
   page carries one green and not three. The two radials are unchanged. */
.uep-hero--green { background: var(--uep-green); }
.uep-hero--green::before {
  background:
    radial-gradient(800px 500px at 85% -10%, var(--uep-overlay-white-18), transparent 60%),
    radial-gradient(600px 400px at 0% 110%, var(--uep-overlay-ink-35), transparent 60%);
}
.uep-hero--green .uep-lead { color: var(--uep-on-dark); }
/* Gold eyebrow on the green field — the FFD brand look. */
.uep-hero--green .uep-overline { color: var(--uep-gold); }
/* The emphasised run in the hero lead is gold too, and still carries its own
   bold weight, so the emphasis does not depend on colour alone. */
.uep-hero--green .uep-lead b,
.uep-hero--green .uep-lead strong { color: var(--uep-gold); }

/* -------- Testimonial card -------- */
/* DEFECT FIX: every testimonial in the tree is marked up as a <figure>, and
   nothing here or in tokens.css reset the UA default `margin: 1em 40px`. The
   cards were rendering 80px narrower than their grid cell and 16px low, so a
   .uep-grid-3 row of them sat inset and misaligned against every other card
   in the kit. It also broke .uep-testimonial--video below, whose whole point
   is the video meeting the card edge. */
.uep-testimonial {
  display: flex; flex-direction: column; gap: var(--space-4);
  margin: 0;
  padding: var(--space-6);
  background: var(--uep-surface);
  border: 1px solid var(--uep-line);
  border-radius: var(--radius-lg);
  height: 100%;
}
.uep-testimonial__quote {
  font-family: var(--font-headline);
  font-size: var(--fs-lead);
  line-height: var(--lh-snug);
  color: var(--uep-ink);
  font-weight: var(--fw-med);
  margin: 0;
}
.uep-testimonial__quote::before {
  content: "\201C"; display: block; font-size: var(--fs-h2); line-height: 0.5; margin-bottom: var(--space-3);
  color: var(--uep-green); font-family: var(--font-headline);
}
.uep-testimonial__meta { display: flex; align-items: center; gap: var(--space-3); margin-top: auto; }
.uep-testimonial__avatar {
  width: 48px; height: 48px; border-radius: var(--radius-pill); object-fit: cover;
  background: var(--uep-surface-alt);
}
.uep-testimonial__name { font-family: var(--font-headline); font-weight: var(--fw-sbd); color: var(--uep-ink); }
.uep-testimonial__role { font-size: var(--fs-small); color: var(--uep-slate); }

/* -------- FAQ / accordion -------- */
.uep-faq { display: grid; gap: var(--space-3); }
.uep-faq__item {
  background: var(--uep-surface);
  border: 1px solid var(--uep-line);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.uep-faq__q {
  width: 100%;
  display: flex; justify-content: space-between; align-items: center; gap: var(--space-4);
  padding: var(--space-5) var(--space-5);
  background: transparent; border: 0;
  font-family: var(--font-headline);
  font-size: var(--fs-h6);
  font-weight: var(--fw-sbd);
  color: var(--uep-ink);
  text-align: left;
  cursor: pointer;
}
.uep-faq__q::after {
  content: "\FF0B";
  font-size: var(--fs-h5); color: var(--uep-green-darker); flex: none;
  transition: transform var(--dur-med) var(--ease-out);
}
.uep-faq__item[open] .uep-faq__q::after { transform: rotate(45deg); }
.uep-faq__a {
  padding: 0 var(--space-5) var(--space-5);
  color: var(--uep-slate);
  line-height: var(--lh-body);
}

/* -------- Progress bar (multi-step forms) -------- */
.uep-progress {
  display: flex; align-items: center; gap: var(--space-3);
  margin-bottom: var(--space-6);
}
.uep-progress__step {
  flex: 1;
  height: 6px;
  background: var(--uep-line);
  border-radius: var(--radius-pill);
}
.uep-progress__step.is-active { background: var(--uep-green); }
.uep-progress__label {
  font-family: var(--font-headline);
  font-size: var(--fs-small);
  font-weight: var(--fw-sbd);
  color: var(--uep-slate);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

/* -------- Stat row -------- */
.uep-stats {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-5);
}
@media (min-width: 768px) {
  .uep-stats { grid-template-columns: repeat(4, 1fr); }
}
.uep-stat__num {
  display: block;
  font-family: var(--font-headline);
  font-size: var(--fs-h2);
  font-weight: var(--fw-blk);
  line-height: 1;
  color: var(--uep-green);
}
.uep-stat__label {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--fs-small);
  color: var(--uep-slate);
  letter-spacing: var(--tracking-slight);
}
.uep-section--dark .uep-stat__num { color: var(--uep-gold); }
.uep-section--dark .uep-stat__label { color: var(--uep-on-dark-muted); }

/* -------- Check list --------
   Bullets are drawn with a glyph + token colours rather than an inline SVG
   data URI, so the list carries no baked-in hex. */
.uep-list {
  list-style: none;
  padding: 0; margin: 0;
  display: grid; gap: var(--space-3);
}
.uep-list li {
  display: flex; align-items: flex-start; gap: var(--space-3);
  line-height: var(--lh-body);
}
.uep-list li::before {
  content: "\2713";
  flex: none;
  display: grid;
  place-items: center;
  width: 22px; height: 22px; margin-top: var(--space-1);
  background: var(--uep-green-tint);
  color: var(--uep-green-darker);
  border-radius: var(--radius-pill);
  font-family: var(--font-headline);
  font-size: var(--fs-overline);
  font-weight: var(--fw-bld);
  line-height: 1;
}
.uep-section--dark .uep-list li::before,
.uep-section--ink .uep-list li::before,
.uep-section--green .uep-list li::before {
  background: var(--uep-overlay-white-08);
  color: var(--uep-gold);
}

/* -------- Utility -------- */
.uep-center-text { text-align: center; }
.uep-mx-auto { margin-inline: auto; }
.uep-mt-4 { margin-top: var(--space-4); }
.uep-mt-6 { margin-top: var(--space-6); }
.uep-mt-8 { margin-top: var(--space-8); }
.uep-mb-0 { margin-bottom: 0; }

/* DEFECT FIX: .uep-hide-on-desktop was an empty ruleset, so the pair only
   half-worked. Both utilities now declare a real display in both states. */
.uep-hide-on-mobile { display: none !important; }
@media (min-width: 768px) {
  .uep-hide-on-mobile { display: revert !important; }
}
.uep-hide-on-desktop { display: revert; }
@media (min-width: 768px) {
  .uep-hide-on-desktop { display: none !important; }
}

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

.uep-skip-link {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px; height: 1px;
  overflow: hidden;
}
.uep-skip-link:focus {
  left: var(--space-4);
  top: var(--space-4);
  width: auto; height: auto;
  padding: var(--space-3) var(--space-4);
  background: var(--uep-ink);
  color: var(--uep-on-dark);
  z-index: 100;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   ===== SITE KIT =====
   Patterns promoted out of the page-local <style> blocks under /pages, each
   consolidated into one token-clean definition, plus the new components the
   site needs. Breakpoints 640 / 768 / 1024 only.
   ========================================================================== */

/* -------- Logo bar --------
   Green band with a centred white logo. Used on pages that carry no nav.
   Deliberately NOT .uep-header: it is not sticky, has no nav, and must not
   inherit the header's brightness(0) logo filter.
   (Promoted from .find-logobar / .coach-logobar / .sprint-logobar.) */
.uep-logobar {
  background: var(--uep-green);
  padding-block: var(--space-4) var(--space-3);
}
.uep-logobar__inner {
  display: flex;
  align-items: center;
  justify-content: center;
}
.uep-logobar__img {
  display: block;
  width: 100%;
  max-width: 200px;
  height: auto;
}

/* -------- Numbered step grid --------
   The one recurring graphic device: a big Poppins-800 numeral.
   (Promoted from .process-grid / .process-card.) */
.uep-step-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}
@media (min-width: 768px) {
  .uep-step-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-6); }
}
@media (min-width: 1024px) {
  .uep-step-grid { grid-template-columns: repeat(3, 1fr); }
}
.uep-step-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  background: var(--uep-surface);
  border: 1px solid var(--uep-line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
}
.uep-step-card__num {
  font-family: var(--font-headline);
  font-weight: var(--fw-blk);
  font-size: var(--fs-h2);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--uep-green);
}
.uep-step-card__title {
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  font-size: var(--fs-h5);
  line-height: var(--lh-snug);
  color: var(--uep-ink);
  margin: 0;
}
.uep-step-card__body {
  color: var(--uep-slate);
  font-size: var(--fs-body);
  margin: 0;
}
.uep-section--dark .uep-step-card,
.uep-section--ink .uep-step-card {
  background: var(--uep-navy);
  border-color: var(--uep-overlay-white-08);
}
.uep-section--dark .uep-step-card__num,
.uep-section--ink .uep-step-card__num { color: var(--uep-gold); }
.uep-section--dark .uep-step-card__title,
.uep-section--ink .uep-step-card__title { color: var(--uep-on-dark); }
.uep-section--dark .uep-step-card__body,
.uep-section--ink .uep-step-card__body { color: var(--uep-on-dark-muted); }

/* -------- Coach strip --------
   Overlapping circular portraits beside a block of copy.
   (Promoted from .coach-intro / .uep-coach-strip.) */
.uep-coach-strip {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
  align-items: center;
}
/* Splits at 1024, not 768. At 768 the `auto` portrait track took 328 of 704px
   and left the copy column at 344px, which set a one-sentence H2 over four
   lines. Below 1024 the portraits stack above the copy. */
@media (min-width: 1024px) {
  .uep-coach-strip { grid-template-columns: auto 1fr; gap: var(--space-6); }
}
.uep-coach-strip__portraits {
  display: flex;
  align-items: center;
}
.uep-coach-strip__portrait {
  width: 96px;
  height: 96px;
  flex: none;
  border-radius: var(--radius-pill);
  border: 4px solid var(--uep-surface);
  object-fit: cover;
  /* Per-image crop, opt-in. Portraits are shot differently and a flat 50% 50%
     cuts the top of some heads; each <img> can set --portrait-pos inline. Same
     idiom as --uep-tiles-pos on the Explore tiles. */
  object-position: var(--portrait-pos, 50% 50%);
  box-shadow: var(--shadow-md);
}
.uep-coach-strip__portrait + .uep-coach-strip__portrait { margin-left: calc(var(--space-5) * -1); }
.uep-coach-strip__body > :last-child { margin-bottom: 0; }
.uep-coach-strip__body p { color: var(--uep-slate); }
.uep-section--dark .uep-coach-strip__body p,
.uep-section--ink .uep-coach-strip__body p,
.uep-section--green .uep-coach-strip__body p { color: var(--uep-on-dark-muted); }

/* -------- Mirror panel (is / isn't) --------
   (Promoted from .uep-mirror on pages/ffd/index.html and pages/ffd/find/index.html.) */
.uep-mirror {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
}
@media (min-width: 768px) {
  .uep-mirror { grid-template-columns: 1fr 1fr; gap: var(--space-7); }
}
.uep-mirror__col {
  background: var(--uep-surface);
  border: 1px solid var(--uep-line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
}
.uep-mirror__col--is { background: var(--uep-surface); }
.uep-mirror__col--isnt { background: var(--uep-surface-alt); }
.uep-mirror__col h3 {
  margin: 0 0 var(--space-4);
  font-size: var(--fs-h4);
}
.uep-mirror__col > :last-child { margin-bottom: 0; }

/* Cross bullet, derived from --uep-danger. */
.uep-list--no li::before {
  content: "\2715";
  background: color-mix(in srgb, var(--uep-danger) 10%, transparent);
  color: var(--uep-danger);
}

/* -------- CTA panel --------
   The one centred element on a page. Designed to sit on .uep-section--green.
   (Promoted from .final-cta / .coaching-adds.) */
.uep-cta-panel {
  max-width: var(--max-w-narrow);
  margin-inline: auto;
  background: var(--uep-surface);
  color: var(--uep-ink);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: var(--shadow-xl);
  text-align: center;
}
.uep-cta-panel h2,
.uep-cta-panel h3 { color: var(--uep-ink); margin-top: 0; }
.uep-cta-panel p { color: var(--uep-slate); margin-inline: auto; max-width: 52ch; }
.uep-cta-panel > :last-child { margin-bottom: 0; }
/* min() so a narrow panel is never forced wider than the viewport. */
.uep-cta-panel .uep-btn { margin-top: var(--space-4); min-width: min(260px, 100%); }
@media (min-width: 768px) {
  .uep-cta-panel { padding: var(--space-7); }
}

/* -------- Trust bar --------
   Ink band under the green hero. 2x2 under 768, 4-up from 768.
   (Promoted from .uep-trustbar / .ffd-trustbar.) */
.uep-trustbar {
  background: var(--uep-ink);
  color: var(--uep-on-dark);
  padding-block: var(--space-6);
}
.uep-trustbar__row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-5);
}
@media (min-width: 768px) {
  .uep-trustbar__row { grid-template-columns: repeat(4, 1fr); gap: var(--space-6); }
}
.uep-trustbar__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.uep-trustbar__num {
  font-family: var(--font-headline);
  font-weight: var(--fw-blk);
  font-size: var(--fs-h3);
  line-height: 1;
  color: var(--uep-gold);
}
.uep-trustbar__label {
  font-family: var(--font-headline);
  font-weight: var(--fw-med);
  font-size: var(--fs-small);
  letter-spacing: var(--tracking-slight);
  color: var(--uep-on-dark-muted);
}

/* -------- Feature row --------
   Media beside copy, alternating with --flip.
   (Promoted from .ffd-framework__row.) */
.uep-feature-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  align-items: center;
}
.uep-feature-row + .uep-feature-row { margin-top: var(--space-8); }
.uep-feature-row__media {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  background: var(--uep-surface-alt);
}
.uep-feature-row__media img,
.uep-feature-row__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.uep-feature-row__body > :last-child { margin-bottom: 0; }
.uep-feature-row__body p { color: var(--uep-slate); max-width: 58ch; }
.uep-feature-row__num {
  display: block;
  font-family: var(--font-headline);
  font-weight: var(--fw-blk);
  font-size: var(--fs-h2);
  line-height: 1;
  color: var(--uep-green);
  margin-bottom: var(--space-3);
}
@media (min-width: 1024px) {
  .uep-feature-row { grid-template-columns: 1fr 1fr; gap: var(--space-8); }
  /* order: -1, not 2. The row is authored copy-first (so the heading precedes
     the image in reading order), which already puts the media in column two —
     `order: 2` was a no-op and the modifier flipped nothing. */
  .uep-feature-row--flip .uep-feature-row__media { order: -1; }
}

/* -------- Feature row, full-width head --------
   The two-column row runs out of media long before it runs out of copy: a
   4:3 image next to seven list items, a kicker and a button ends roughly a
   third of the way down and leaves a hole under it.

   --head-span lifts the heading block out of the copy column and spans it
   across both, which shortens the copy column by the height of the heading,
   and .uep-feature-row__aside lets the media column carry trailing copy of its
   own under the image. The two columns then finish at about the same line.

   align-items: start, not center. With the head lifted out, centring the two
   remaining columns floated the short one and reopened the gap this modifier
   exists to close. Below 1024 the row is a single column and everything falls
   back to source order: head, copy, image, kicker. */
.uep-feature-row__aside { display: flex; flex-direction: column; gap: var(--space-5); }
.uep-feature-row__aside > :last-child { margin-bottom: 0; }
/* The head is no longer a child of __body, so it no longer inherits that
   block's paragraph treatment. Restate it. */
.uep-feature-row__head > :last-child { margin-bottom: 0; }
.uep-feature-row__head p { color: var(--uep-slate); max-width: 58ch; }
@media (min-width: 1024px) {
  .uep-feature-row--head-span {
    grid-template-areas:
      "head head"
      "body aside";
    align-items: start;
    row-gap: var(--space-7);
  }
  .uep-feature-row--head-span > .uep-feature-row__head  { grid-area: head; }
  .uep-feature-row--head-span > .uep-feature-row__body  { grid-area: body; }
  .uep-feature-row--head-span > .uep-feature-row__aside { grid-area: aside; }
}

/* -------- White button, for green and dark grounds -------- */
.uep-btn--white {
  background: var(--uep-surface);
  color: var(--uep-ink);
  box-shadow: var(--shadow-glow-ink);
}
.uep-btn--white:hover {
  background: var(--uep-green-tint);
  color: var(--uep-ink);
  transform: translateY(-1px);
}
/* Member Login goes gold on hover. Selected by DESTINATION rather than by the
   --white modifier, because that modifier is shared: it also dresses the
   "Show Me The Program" and "Book My Call" hero buttons, which should keep the
   default tint. Both Member Login instances — the desktop header CTA and the
   block button inside the mobile nav — point at the member portal, so the href
   is what they actually have in common.
   Ink text on gold, not white: gold is a light ground and white on it is
   unreadable. */
.uep-btn--white[href*="member.unemployableproperty.com.au"]:hover,
.uep-btn--white[href*="member.unemployableproperty.com.au"]:focus-visible {
  background: var(--uep-gold);
  color: var(--uep-ink);
}

/* -------- Iframe embeds --------
   Sized with aspect-ratio + min-height rather than a fixed height, so a
   third-party form or calendar can grow without being clipped.
   (Promoted from .find-hero__calendar iframe / .submit-embed / .coach-form-embed.) */
.uep-embed {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  min-height: 320px;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--uep-surface-alt);
  box-shadow: var(--shadow-md);
}
.uep-embed > iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* Unfilled embed slot. Deliberately LOUD rather than a neutral grey box: this
   ships in the page, and a quiet placeholder is one that reaches production
   because nobody noticed it. It also carries no play button — the previous
   occupant of this slot was withdrawn precisely because a play control over
   Jeff T's photograph claimed footage of him that does not exist, and a
   control that does nothing makes the same claim. */
.uep-embed--placeholder {
  display: grid;
  place-items: center;
  /* place-items centres each row WITHIN itself; the two rows still stack from
     the top of a 16:9 box. align-content centres the pair as a block. */
  align-content: center;
  text-align: center;
  padding: var(--space-6);
  border: 2px dashed var(--uep-line);
  background: var(--uep-surface-alt);
  box-shadow: none;
}
.uep-embed__placeholder-label {
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  font-size: var(--fs-overline);
  letter-spacing: var(--tracking-xwide);
  text-transform: uppercase;
  color: var(--uep-slate);
  margin: 0 0 var(--space-2);
}
.uep-embed__placeholder-note {
  font-size: var(--fs-small);
  color: var(--uep-muted);
  margin: 0;
  max-width: 34ch;
}

.uep-embed__caption {
  margin-top: var(--space-3);
  font-size: var(--fs-small);
  color: var(--uep-slate);
}
/* -------- Calendar embed --------
   The GHL booking widget measures its own content and writes an inline height
   onto the iframe (link.msgsndr.com/js/form_embed.js runs iFrameResize with
   sizeHeight, whose callback sets iframe.style.height). An inline height beats
   any stylesheet rule, so the iframe WILL grow past whatever box we give it —
   and .uep-embed above sets overflow: hidden, which would cut the overflow off
   silently, with no scrollbar and no way to reach the confirm button. On a
   phone that is roughly a third of the widget unreachable.

   So this variant does not box the iframe at all. The floor moves off the
   container and onto the iframe as a min-height (still no fixed height), the
   iframe comes out of the absolute layer into normal flow, and the container's
   height follows it. Same shape as the known-good legacy embeds. */
.uep-embed--calendar {
  aspect-ratio: auto;
  min-height: 0;
  overflow: visible;
  background: none;
  box-shadow: none;
  border-radius: 0;
}
.uep-embed--calendar > iframe {
  position: static;
  height: auto;
  min-height: 760px;
  border-radius: var(--radius-lg);
  background: var(--uep-surface-alt);
  box-shadow: var(--shadow-md);
}
@media (min-width: 768px) {
  .uep-embed--calendar > iframe { min-height: 860px; }
}
@media (min-width: 1024px) {
  .uep-embed--calendar > iframe { min-height: 900px; }
}

/* -------- Social row -------- */
.uep-social {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  margin: 0;
}
.uep-social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  color: var(--uep-on-dark-muted);
  transition: color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
/* Height-locked, width auto — the five marks are drawn from the official
   artwork and do NOT share an aspect ratio (YouTube is 28.57:20, the rest are
   square). Forcing a 20x20 box squashes the YouTube mark. The 44px link box
   keeps the row's rhythm even though the glyphs differ in width.
   fill: currentColor is what keeps these monochrome and lets them go gold on
   hover — which is why these stay inline SVG rather than becoming <img> files
   like the full-colour marks in the subscribe band. An <img> cannot inherit
   colour. */
.uep-social__link svg {
  width: auto;
  height: 20px;
  fill: currentColor;
  display: block;
}
.uep-social__link:hover {
  color: var(--uep-gold);
  background: var(--uep-overlay-white-08);
}
.uep-section--alt .uep-social__link,
.uep-section--warm .uep-social__link { color: var(--uep-slate); }
.uep-section--alt .uep-social__link:hover,
.uep-section--warm .uep-social__link:hover {
  color: var(--uep-green);
  background: var(--uep-green-tint);
}

/* -------- Slim footer (logo + legal + social, no menu grid) -------- */
.uep-footer--slim {
  padding-block: var(--space-7) var(--space-6);
}
.uep-footer--slim .uep-footer__grid { display: none; }
.uep-footer--slim .uep-footer__inner {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
  align-items: center;
}
@media (min-width: 768px) {
  .uep-footer--slim .uep-footer__inner {
    grid-template-columns: auto 1fr auto;
    gap: var(--space-6);
  }
}
.uep-footer--slim .uep-footer__legal {
  padding-top: 0;
  justify-content: flex-start;
}
/* The inner grid is auto 1fr auto — logo, social, legal — so the social row
   sits in the only flexible track. Ending it flush right parked the icons
   against the legal text with every pixel of slack on the left. Centre them
   in their own track instead, so the three blocks read as three blocks. */
@media (min-width: 768px) {
  .uep-footer--slim .uep-social { justify-content: center; }
}

/* -------- Episode card (podcasts) -------- */
.uep-episode-card {
  display: flex;
  flex-direction: column;
  background: var(--uep-surface);
  border: 1px solid var(--uep-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  height: 100%;
  transition: box-shadow var(--dur-med) var(--ease-out),
              border-color var(--dur-med) var(--ease-out);
}
.uep-episode-card:hover {
  box-shadow: var(--shadow-lg);
  border-color: transparent;
}
.uep-episode-card__thumb {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--uep-surface-alt);
}
.uep-episode-card__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.uep-episode-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  flex: 1;
}
.uep-episode-card__title {
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  font-size: var(--fs-h5);
  line-height: var(--lh-snug);
  color: var(--uep-ink);
  margin: 0;
}
.uep-episode-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin-top: auto;
  font-family: var(--font-headline);
  font-size: var(--fs-small);
  font-weight: var(--fw-med);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--uep-slate);
}
.uep-episode-card__meta a { color: var(--uep-green-darker); }

/* -------- Case study card -------- */
.uep-case-card {
  display: flex;
  flex-direction: column;
  background: var(--uep-surface);
  border: 1px solid var(--uep-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  height: 100%;
}
.uep-case-card__media {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--uep-surface-alt);
}
.uep-case-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.uep-case-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-5) var(--space-6);
  flex: 1;
}
.uep-case-card__name {
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  font-size: var(--fs-h5);
  line-height: var(--lh-snug);
  color: var(--uep-ink);
  margin: 0;
}
.uep-case-card__meta {
  font-size: var(--fs-small);
  color: var(--uep-slate);
}
.uep-case-card__outcome {
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: 1px solid var(--uep-line);
  font-family: var(--font-headline);
  font-weight: var(--fw-sbd);
  font-size: var(--fs-body);
  color: var(--uep-green-darker);
}

/* -------- Event card (date block left, content right) -------- */
.uep-event-card {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-5);
  align-items: start;
  padding-block: var(--space-5);
  border-bottom: 1px solid var(--uep-line);
}
.uep-event-card__date {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-width: 84px;
  padding: var(--space-3) var(--space-4);
  background: var(--uep-green-tint);
  border-radius: var(--radius-md);
  font-family: var(--font-headline);
  line-height: 1;
  text-align: center;
}
.uep-event-card__day {
  font-weight: var(--fw-blk);
  font-size: var(--fs-h4);
  color: var(--uep-green-darker);
}
.uep-event-card__month {
  font-weight: var(--fw-sbd);
  font-size: var(--fs-overline);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--uep-green-darker);
}
.uep-event-card__body > :last-child { margin-bottom: 0; }
.uep-event-card__body h3 {
  font-size: var(--fs-h5);
  margin: 0 0 var(--space-2);
}
.uep-event-card__body p { color: var(--uep-slate); }

/* -------- Media rail --------
   Horizontal scroll rail that bleeds off the RIGHT edge only. Native swipe,
   scroll-snap, edge fade. No arrows, no dots.
   The negative right margin is exactly the container's inline padding, so the
   rail's right edge lands on the viewport edge and never past it — at 375px:
   container 375 - (24 + 24) padding = 327 content, rail 327 + 24 = 351, right
   edge at 24 + 351 = 375. Its own overflow-x: auto contains the children, so
   the page body never scrolls sideways. */
.uep-media-rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 78%;
  gap: var(--space-4);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-inline: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline-start: var(--space-1);
  padding-block: var(--space-3) var(--space-5);
  margin-right: calc(-1 * var(--space-5));
  padding-right: var(--space-5);
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* `black` here is mask alpha, not a brand colour — the idiomatic opaque stop. */
  -webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - var(--space-7)), transparent 100%);
  mask-image: linear-gradient(to right, black 0, black calc(100% - var(--space-7)), transparent 100%);
}
/* The rail's right border edge lands exactly on the viewport edge, so an
   outward focus ring would be clipped by the propagated overflow-x. */
.uep-media-rail:focus-visible { outline-offset: -4px; }
.uep-media-rail::-webkit-scrollbar { display: none; }
.uep-media-rail > * {
  scroll-snap-align: start;
  min-width: 0;
}
@media (min-width: 640px) {
  .uep-media-rail { grid-auto-columns: 52%; gap: var(--space-5); }
}
@media (min-width: 768px) {
  .uep-media-rail {
    grid-auto-columns: 42%;
    margin-right: calc(-1 * var(--space-6));
    padding-right: var(--space-6);
  }
}
@media (min-width: 1024px) {
  .uep-media-rail { grid-auto-columns: 30%; gap: var(--space-6); }
}

/* -------- Legal document wrapper -------- */
.uep-legal-doc {
  max-width: var(--max-w-body);
  color: var(--uep-slate);
  font-size: var(--fs-body);
  line-height: var(--lh-loose);
}
.uep-legal-doc > :first-child { margin-top: 0; }
.uep-legal-doc > :last-child { margin-bottom: 0; }
.uep-legal-doc h2 {
  font-size: var(--fs-h4);
  color: var(--uep-ink);
  margin: var(--space-8) 0 var(--space-4);
  padding-top: var(--space-5);
  border-top: 1px solid var(--uep-line);
}
.uep-legal-doc h2:first-of-type { border-top: 0; padding-top: 0; margin-top: 0; }
.uep-legal-doc h3 {
  font-size: var(--fs-h6);
  color: var(--uep-ink);
  margin: var(--space-6) 0 var(--space-3);
}
.uep-legal-doc p { margin: 0 0 var(--space-4); }
.uep-legal-doc strong { color: var(--uep-ink); font-weight: var(--fw-sbd); }
/* overflow-wrap: anywhere, not break-word. The privacy policy quotes bare
   opt-out URLs as their own link text ("https://www.facebook.com/settings/?tab=ads"),
   which is one unbreakable 350px token. break-word would let it wrap but still
   report the full token as min-content, so the ancestor track stayed 437px wide
   and /privacy/ scrolled sideways at 375 — inside the supported range. `anywhere`
   feeds the break back into intrinsic sizing, so the column shrinks with the
   viewport. */
.uep-legal-doc a {
  text-decoration: underline;
  text-underline-offset: 3px;
  overflow-wrap: anywhere;
}
.uep-legal-doc ol,
.uep-legal-doc ul {
  margin: 0 0 var(--space-5);
  padding-left: var(--space-5);
  display: grid;
  gap: var(--space-2);
}
.uep-legal-doc li { padding-left: var(--space-1); }
.uep-legal-doc li::marker { color: var(--uep-green-darker); font-weight: var(--fw-sbd); }
.uep-legal-doc table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 var(--space-6);
  font-size: var(--fs-small);
}
.uep-legal-doc caption {
  text-align: left;
  font-family: var(--font-headline);
  font-size: var(--fs-small);
  font-weight: var(--fw-sbd);
  color: var(--uep-ink);
  padding-bottom: var(--space-3);
}
.uep-legal-doc th,
.uep-legal-doc td {
  text-align: left;
  vertical-align: top;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--uep-line);
}
.uep-legal-doc th {
  font-family: var(--font-headline);
  font-weight: var(--fw-sbd);
  color: var(--uep-ink);
  background: var(--uep-surface-alt);
}
.uep-legal-doc__updated {
  font-size: var(--fs-small);
  color: var(--uep-slate);
  margin-bottom: var(--space-6);
}
/* Wide tables scroll inside their own box, never the page. */
.uep-legal-doc__scroll { overflow-x: auto; }

/* -------- Reveal on scroll --------
   DEFECT FIX: previously [data-uep-reveal] set opacity: 0 unconditionally, so
   a visitor with JS disabled or a failed app.js load saw a blank page. The
   rules are now gated on .uep-js, which app.js stamps on <html> before
   anything else runs. No JS, no hiding. */
.uep-js [data-uep-reveal] {
  opacity: 0;
  transform: translateY(var(--space-4));
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
  will-change: opacity, transform;
}
.uep-js [data-uep-reveal].is-revealed {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .uep-js [data-uep-reveal],
  .uep-js [data-uep-reveal].is-revealed {
    opacity: 1;
    transform: none;
    transition: none;
    will-change: auto;
  }
}

/* -------- Notification top bar --------
   Optional announcement / event strip that sits ABOVE the green header, for
   events, announcements and promos. Pages that do not want it simply omit the
   markup — nothing else in the kit depends on it existing.

   Gold ground, ink text: --uep-ink on --uep-gold measures 11.14:1, the
   high-contrast half of the brand pair, so a single --fs-small line reads
   cleanly at the very top of the page.

   STICKY DECISION — the bar does NOT stick, it scrolls away.
   It is an announcement, not navigation, so it must not spend the whole
   session eating 44px of a phone viewport. Because it stays in normal flow
   directly ABOVE .uep-header, the header's own `top: 0` catches the viewport
   at the exact scroll offset the bar finishes leaving it: the header's natural
   offset equals the bar's height, so it pins the instant the bar clears. No
   gap can open between them and neither can overlap the other.

   It also means tokens.css's `scroll-padding-top: var(--space-9)` (96px) is
   still correct and needs no change: the ~73px header is the only thing
   covering the viewport top once any anchor jump has settled, because the bar
   is off-screen by then. Making the bar sticky WOULD have invalidated that
   offset — a second reason not to.

   The z-index below the header's 40 is belt-and-braces for the momentary
   overlap an iOS rubber-band overscroll can force at scrollTop < 0. */
.uep-topbar {
  position: relative;
  z-index: 39;
  background: var(--uep-gold);
  color: var(--uep-ink);
}
.uep-topbar[hidden] { display: none; }

.uep-topbar__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* border-box, so this INCLUDES the padding — 40px total, which is the height
     the bar ran at before the dismiss button was removed. align-items:center
     is what keeps the single line of copy optically centred within it. */
  min-height: 40px;
  padding-block: var(--space-1);
}
/* Single centred child now that the dismiss button is gone, so no balancing
   spacer is needed. align-items:center on the flex parent is what vertically
   centres the copy; the min-height is a floor, not a fixed height. */
.uep-topbar__copy {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  text-align: center;
  font-size: var(--fs-small);
  line-height: var(--lh-snug);
  color: var(--uep-ink);
  text-wrap: balance;
}
/* Under 640 the copy is allowed to run to two lines. min-height on the inner
   is a floor, not a fixed height, and both flex children are vertically
   centred, so the bar simply grows and the header below it moves down with
   it — nothing is clipped and nothing overlaps. */

/* Inherits colour on purpose: a second hue on a gold ground would be the
   third colour in a 44px strip. The underline carries the affordance. */
.uep-topbar__link {
  color: inherit;
  font-weight: var(--fw-sbd);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.uep-topbar__link:hover { color: inherit; }

/* -------- Full-bleed video hero --------
   A cinematic intro that fills the viewport above everything else: full-bleed
   autoplaying video, the wordmark over it, and a SCROLL cue anchoring to the
   section below.

   HEIGHT: 100svh minus the chrome above it, so the SCROLL cue lands ON the
   fold rather than under it. svh (small viewport height) not vh — on iOS, vh
   is the height with the URL bar HIDDEN, so a 100vh hero is taller than the
   visible area on first paint and the cue starts off-screen. Measured chrome
   is 40px top bar + 85px header = 125px; the token below is the single place
   to change it if either grows.

   The header is sticky at z-index 40 and scrolls away with the page, so it
   sits above this section in flow rather than over it. That is deliberate:
   a green bar over full-bleed video reads as a banner, not as chrome.

   AUTOPLAY: muted + playsinline are both REQUIRED. Every mobile browser
   blocks autoplay without them, and iOS additionally fullscreens an
   un-playsinline video. loop makes it ambient; no controls, because there is
   nothing to control — the real videos live further down the page. */
.uep-videohero {
  --uep-videohero-chrome: 125px;
  position: relative;
  min-height: calc(100svh - var(--uep-videohero-chrome));
  background: var(--uep-ink);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
}
.uep-videohero__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Ink scrim, heaviest at the bottom where the wordmark and cue sit. Without
   it the gold cue can land on a bright frame and vanish — the video is not a
   fixed background, so contrast cannot be checked once and trusted. */
.uep-videohero::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Only --uep-overlay-ink-35 / -55 / -95 exist in tokens.css. Do not reach
     for -20/-70/-85 etc: an undefined var() silently collapses the whole
     declaration and the scrim disappears. */
  background: linear-gradient(
    to bottom,
    var(--uep-overlay-ink-35) 0%,
    transparent 25%,
    var(--uep-overlay-ink-35) 55%,
    var(--uep-overlay-ink-55) 78%,
    var(--uep-overlay-ink-95) 100%
  );
  pointer-events: none;
}
.uep-videohero__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-7);
  padding-block: var(--space-8) var(--space-6);
  text-align: center;
}
.uep-videohero__mark {
  width: min(560px, 72vw);
  height: auto;
  display: block;
}
/* The scroll cue is a real anchor, not a decorative flourish: it is keyboard
   reachable and it actually navigates. */
.uep-videohero__cue {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  color: var(--uep-gold);
  font-family: var(--font-headline);
  font-size: var(--fs-overline);
  font-weight: var(--fw-bld);
  letter-spacing: var(--tracking-xwide);
  text-transform: uppercase;
  text-decoration: none;
}
.uep-videohero__cue:hover { color: var(--uep-gold); }
.uep-videohero__cue svg {
  width: 20px;
  height: 26px;
  animation: uep-cue-bob 2.2s var(--ease-out) infinite;
}
@keyframes uep-cue-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .uep-videohero__cue svg { animation: none; }
}
@media (min-width: 768px) {
  .uep-videohero__inner { gap: var(--space-8); padding-block: var(--space-9) var(--space-7); }
}

/* -------- Video testimonial --------
   Modifier on .uep-testimonial so a video card and a text card can sit in the
   same .uep-grid-3 and read as one row. The card frame supplies the border and
   the radius; the video fills the top edge to edge.

   NO MARKUP USES THIS TODAY, and that is deliberate rather than an oversight.
   Every member testimonial that carried a video was playing a file recorded of
   somebody else — five source files were standing in for eleven different
   claims — so the videos were withdrawn and those cards now ship as text and a
   real headshot. The rules below are kept, unchanged and unreferenced, because
   they are the finished component and this tree has no git history to recover
   them from: restore the <div class="uep-video" data-uep-video> block inside
   the figure and add `uep-testimonial--video` to it the day real per-member
   footage exists. Anything else here that no markup references has been
   deleted, not parked. */
.uep-testimonial--video {
  padding: 0;
  overflow: hidden;
}
/* .uep-video normally ships as a standalone hero frame — its own radius and
   drop shadow would fight the card it is now nested inside, and its flex
   default would let it stretch. Scoped off here only. */
.uep-testimonial--video .uep-video {
  border-radius: 0;
  box-shadow: none;
  flex: none;
}
.uep-testimonial--video .uep-video video { object-fit: cover; }
/* The 84px hero play button is out of scale inside a third-width card. */
.uep-testimonial--video .uep-video__play-icon { width: 56px; height: 56px; }
.uep-testimonial--video .uep-video__play-icon svg { width: 22px; height: 22px; }
/* The video IS the testimonial here; the text under it is the caption. So the
   quote steps down to body size and drops the big green quote mark, which
   would otherwise announce the caption as the primary evidence. */
.uep-testimonial--video .uep-testimonial__quote {
  font-size: var(--fs-body);
  padding-inline: var(--space-6);
}
.uep-testimonial--video .uep-testimonial__quote::before { content: none; }
.uep-testimonial--video .uep-testimonial__meta {
  padding: 0 var(--space-6) var(--space-6);
}

/* -------- Featured testimonial --------
   One testimonial given a full section: media on one side, a long quote on the
   other. Keeps the green quote mark — here the words ARE the evidence.
   Reuses .uep-testimonial__meta / __avatar / __name / __role verbatim. */
.uep-testimonial-feature {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  align-items: center;
  /* Same UA <figure> margin as .uep-testimonial above — this block is a
     figure too, and a 40px inline margin would break its 1024 split. */
  margin: 0;
}
.uep-testimonial-feature blockquote { margin: 0; }
@media (min-width: 1024px) {
  .uep-testimonial-feature {
    grid-template-columns: 1.15fr 1fr;
    gap: var(--space-8);
  }
}
/* No-media variant. The feature block is normally a photograph beside a quote;
   with the photograph gone the 1.15fr/1fr split would strand the words in the
   left column against dead space, so this one stays single-column at every
   width. Used where a member's full-size portrait has been withdrawn but the
   testimonial itself still stands. */
.uep-testimonial-feature--nomedia { grid-template-columns: 1fr; }
@media (min-width: 1024px) {
  .uep-testimonial-feature--nomedia { grid-template-columns: 1fr; }
}
/* Still-image media column. NO fixed aspect-ratio and NO object-fit on purpose:
   these are real member headshots at native ratios from 1.07 to 1.40, so a 16/9
   frame would either pillarbox them against the ink ground (object-fit: contain)
   or crop the top of the head off (cover). Letting the image keep its own ratio
   costs a little height variation between pages and cuts nothing off anybody. */
.uep-testimonial-feature__media {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
  background: var(--uep-surface-alt);
  box-shadow: var(--shadow-lg);
}
/* When the slot holds a player rather than a bare <img>, the class is on a
   WRAPPER around the player and its caption. The radius, ground and shadow
   belong to the player — .uep-video draws its own — so the wrapper drops them
   or it paints a second shadow around the caption text as well. */
.uep-testimonial-feature__media:has(> .uep-video) {
  border-radius: 0;
  background: none;
  box-shadow: none;
}

.uep-testimonial-feature__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}
.uep-testimonial-feature__quote {
  font-family: var(--font-headline);
  font-size: var(--fs-h4);
  line-height: var(--lh-snug);
  font-weight: var(--fw-med);
  letter-spacing: var(--tracking-tight);
  color: var(--uep-ink);
  margin: 0;
}
.uep-testimonial-feature__quote::before {
  content: "\201C";
  display: block;
  font-family: var(--font-headline);
  font-size: var(--fs-h2);
  line-height: 0.5;
  margin-bottom: var(--space-3);
  color: var(--uep-green);
}
.uep-testimonial-feature .uep-testimonial__meta { margin-top: 0; }

.uep-section--ink .uep-testimonial-feature__quote,
.uep-section--dark .uep-testimonial-feature__quote { color: var(--uep-on-dark); }
.uep-section--ink .uep-testimonial-feature .uep-testimonial__name,
.uep-section--dark .uep-testimonial-feature .uep-testimonial__name { color: var(--uep-on-dark); }
.uep-section--ink .uep-testimonial-feature .uep-testimonial__role,
.uep-section--dark .uep-testimonial-feature .uep-testimonial__role { color: var(--uep-on-dark-muted); }

/* -------- Episode rows --------
   Big-thumbnail podcast rows on an ink ground: artwork left, title and
   description right, platform links on the baseline. Left-aligned, flat, one
   row per episode — deliberately not a card grid.
   The section wrapper supplies the ground (.uep-section--ink). */
.uep-epirow-list {
  display: grid;
  gap: var(--space-8);
}
@media (min-width: 768px) {
  .uep-epirow-list { gap: var(--space-9); }
}

/* minmax(0, Nfr) not plain Nfr: a fr track floors at min-content, and an
   unbroken episode title would otherwise push the media column past its
   share and overflow the row. */
.uep-epirow {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  align-items: start;
}
/* The split fires at 1024, NOT 768. At 768 the 5fr media column resolves to
   ~280px — a 158px-tall thumbnail against a 244-317px body, so every row ended
   with a void under the artwork. --fs-h3 also steps up at 768, so the headline
   hit its largest size at the exact width the artwork hit its smallest. The
   768-1023 band keeps the full-width stacked thumbnail instead, which is the
   "big thumbnail" this component is named for. At 1024 the same ratio is
   balanced (387x217 media against a 196-251 body). */
@media (min-width: 1024px) {
  .uep-epirow {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--space-6);
  }
}

.uep-epirow__media {
  position: relative;
  display: block;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--uep-navy-deep);
}
.uep-epirow__thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  transition: transform var(--dur-med) var(--ease-out);
}
.uep-epirow__media:hover .uep-epirow__thumb,
.uep-epirow__media:focus-visible .uep-epirow__thumb { transform: scale(1.03); }

/* inset: 0 + margin: auto centres in both axes without a transform, so the
   hover scale below is the only transform on the element and reduced motion
   can cancel it outright. pointer-events: none keeps the whole tile a single
   click target for the link that wraps it. */
.uep-epirow__play {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-pill);
  background: var(--uep-gold);
  display: grid;
  place-items: center;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  transition: transform var(--dur-med) var(--ease-out);
}
/* Pure-CSS triangle — no icon file, no inline SVG to keep in sync. The 4px
   nudge optically centres a triangle inside a circle. */
.uep-epirow__play::after {
  content: "";
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 11px 0 11px 18px;
  border-color: transparent transparent transparent var(--uep-ink);
  margin-left: var(--space-1);
}
.uep-epirow__media:hover .uep-epirow__play,
.uep-epirow__media:focus-visible .uep-epirow__play { transform: scale(1.08); }

.uep-epirow__body { min-width: 0; }

.uep-epirow__title {
  font-family: var(--font-headline);
  font-weight: var(--fw-blk);
  font-size: var(--fs-h4);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  text-transform: uppercase;
  color: var(--uep-on-dark);
  margin: 0 0 var(--space-3);
}
@media (min-width: 640px) {
  .uep-epirow__title { font-size: var(--fs-h3); }
}
.uep-epirow__title a { color: inherit; }
.uep-epirow__title a:hover { color: var(--uep-gold); }

.uep-epirow__desc {
  color: var(--uep-on-dark-muted);
  margin: 0 0 var(--space-3);
}
/* .is-clamped is added by initTruncate ONLY after it has measured the text
   past two line-boxes, so a short description is never clamped and never
   grows a dead button. */
.uep-epirow__desc.is-clamped:not(.is-expanded) {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* min-height carries the 44px target floor: at --fs-small with no padding this
   button was a ~22px-tall hit area, and it is the control most easily missed
   because JS is what reveals it. The bottom margin drops by the height the
   target now adds, so the rhythm below the row is unchanged. */
.uep-epirow__more {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin: 0 0 var(--space-4);
  padding: 0;
  background: none;
  border: 0;
  color: var(--uep-gold);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-sbd);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
/* Must follow the display above — the button ships hidden and initTruncate
   un-hides it only when the text actually overflows. */
.uep-epirow__more[hidden] { display: none; }

/* The row holds two small items — a ~78px date and a ~92px chip pair — so
   space-between threw them to opposite ends of a 541px (1024) / 644px (1440)
   column and left a 374-473px empty run between them. The chips then read as
   annotating nothing. They sit together under the copy instead, left-aligned
   like every other element in the row. */
.uep-epirow__cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-5);
}

/* The row's one labelled call to action, first in the cta row and first in the
   source order after the description. It repeats the destination the artwork
   already links to, on purpose: the artwork is a picture with no visible label,
   and "Watch Episode →" is the only thing in the row that says out loud what
   clicking does.
   min-height carries the same 44px target floor as .uep-epirow__more and the
   platform chips — at --fs-small with no padding this is a ~17px-tall hit area
   otherwise. It costs the layout nothing: the 44px chips in the same flex row
   already set that height. */
.uep-epirow__watch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  font-size: var(--fs-small);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--uep-gold);
  text-decoration: none;
  white-space: nowrap;
}
.uep-epirow__watch:hover,
.uep-epirow__watch:focus-visible {
  text-decoration: underline;
  text-underline-offset: 4px;
}
/* The arrow is the only thing that moves. Underline on hover carries the state
   for anyone who never sees the 4px travel — reduced motion, or a device with
   no hover at all — so the movement is decoration and never the signal. */
.uep-epirow__arrow {
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-out);
}
.uep-epirow__watch:hover .uep-epirow__arrow,
.uep-epirow__watch:focus-visible .uep-epirow__arrow {
  transform: translateX(4px);
}

.uep-epirow__platforms {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* 44px ring — the kit's touch-target floor. (This comment used to reference
   .uep-topbar__dismiss as the matching size; that control no longer exists.) */
.uep-epirow__platform {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--uep-overlay-white-18);
  transition: border-color var(--dur-fast) var(--ease-out);
}
/* Hover moves the RING, not the mark. A brand logo recoloured on hover stops
   being that brand's logo, so the affordance is the only thing that changes. */
.uep-epirow__platform:hover,
.uep-epirow__platform:focus-visible {
  border-color: var(--uep-gold);
}
/* No fill here. Each mark carries its own official colours as fill ATTRIBUTES
   on its shapes — a fill declared in CSS would inherit down and repaint all of
   them one flat colour. Those attributes are also the only lawful home for a
   brand hex: validate.py's tokens-only scan reads <style> blocks and inline
   style attributes and nothing else, so the same colour written as a fill
   declaration inside an inline style attribute fails the build, while a
   presentation attribute on the path is exactly what a brand asset looks like.
   22px, not 18px: at 18px inside a 44px ring the Apple Podcasts glyph loses its
   inner arc and reads as a smudge. */
/* Height-locked, width auto. These are the OFFICIAL logo files, not redrawn
   paths, so they keep their true aspect ratios — YouTube's is 28.57:20 and the
   other two are square. A fixed 22x22 box would squash the YouTube mark.
   They are <img> rather than inline SVG because the hover only moves the ring
   (see below), so no mark ever needs to inherit a colour — which is the only
   reason inline would be required. */
.uep-epirow__platform img {
  width: auto;
  height: 22px;
  display: block;
}
/* Optical sizing, not geometric. The YouTube mark is a wide rounded rectangle
   (28.57:20), so at the same 22px height it covers noticeably more area than
   the two circular marks and reads heavier in the row. Dropping it to 18px
   brings its visual weight into line without touching the files. Scoped to the
   episode chips — the subscribe band gives each mark its own card with a label,
   so the imbalance does not arise there. */
.uep-epirow__platform img[src$="logo-youtube.svg"] {
  height: 18px;
}

/* Every transform and transition declared above, cancelled. The global
   reduced-motion reset near the top of this file kills transitions but not
   the transforms they animate, so a hover would still snap the thumbnail and
   the play button. These rules remove the movement itself. */
@media (prefers-reduced-motion: reduce) {
  .uep-epirow__thumb,
  .uep-epirow__media:hover .uep-epirow__thumb,
  .uep-epirow__media:focus-visible .uep-epirow__thumb,
  .uep-epirow__play,
  .uep-epirow__media:hover .uep-epirow__play,
  .uep-epirow__media:focus-visible .uep-epirow__play,
  .uep-epirow__platform,
  .uep-epirow__arrow,
  .uep-epirow__watch:hover .uep-epirow__arrow,
  .uep-epirow__watch:focus-visible .uep-epirow__arrow {
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   SUBSCRIBE BAND — .uep-subscribe
   Appended as a self-contained block on purpose: the EPIROW rules above are
   being edited concurrently, so nothing here reaches back into them.

   Used on /podcasts/ (below the episode list) and / (below "Most recent
   episodes"). The band pairs with .uep-section--green in the markup rather
   than painting its own background, so it inherits the two things that rule
   already settles: the on-dark heading colour, and --uep-ring flipping to
   white. The ring is correct white here because outline-offset paints it
   OUTSIDE the card, on the green ground (3.87:1), not on the white card.

   Brand green behind white text is a settled client decision. Do not darken
   the band and do not restate the ratio here.
   ========================================================================== */

.uep-subscribe__head {
  text-align: center;
  max-width: var(--max-w-body);
  margin-inline: auto;
  margin-bottom: var(--space-7);
}

/* Uppercase is the reference design's, so the tracking has to be opened up:
   an uppercase headline set at heading tracking (--tracking-tight) closes the
   counters and reads as one block. */
.uep-subscribe__title {
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  margin-bottom: 0;
  text-wrap: balance;
}

/* THREE cards, and the grid says so. Google Podcasts is deliberately absent —
   it was folded into YouTube Music in 2024 and a fourth column would be a dead
   link. One column below 768; from 768 the three sit across.

   The 640 step caps the STACKED column instead of splitting it. Measured at a
   700px viewport the full-width card came out 652px wide holding a 36px mark
   and one word — an empty bar, not a card. A max-width on the list could not
   fix that: --max-w-narrow is 720px and the container is only 719px at 767px
   wide, so the cap never bit. Constraining the grid TRACK and centring it does.
   Splitting to two columns was the other option and is worse: three cards over
   two columns always orphans one. */
.uep-subscribe__list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 640px) {
  .uep-subscribe__list {
    grid-template-columns: minmax(0, 420px);
    justify-content: center;
  }
}

@media (min-width: 768px) {
  .uep-subscribe__list {
    grid-template-columns: repeat(3, 1fr);
    justify-content: normal;
    gap: var(--space-6);
  }
}

/* The whole card is the <a>, so the entire white surface is the target rather
   than the wordmark alone. display:flex + height:100% keeps the three level
   when one name wraps. */
.uep-subscribe__card {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  height: 100%;
  min-height: 104px;
  padding: var(--space-6) var(--space-5);
  background: var(--uep-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  color: var(--uep-ink);
  font-family: var(--font-headline);
  font-size: var(--fs-h5);
  font-weight: var(--fw-sbd);
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-tight);
  text-align: center;
  transition: transform var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out);
}

/* Same lift and shadow step as .uep-card, so the two hover identically.
   The colour is restated because the global `a:hover` rule turns links green,
   which would recolour the wordmark on hover for no reason. */
.uep-subscribe__card:hover,
.uep-subscribe__card:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
  color: var(--uep-ink);
}

/* Brand marks carry their own colour in fill="" attributes on the paths, which
   is why no fill is declared here — a fill on the class would beat the
   presentation attribute and flatten all three to one colour. */
/* Height-locked, width auto. The three marks have DIFFERENT native aspect
   ratios — YouTube's is 28.57:20, Apple's and Spotify's are 1:1 — so a fixed
   square box squashes the YouTube mark. Constraining height and letting width
   follow the viewBox keeps each logo at its true proportions, which is a brand
   requirement, not a preference. */
.uep-subscribe__logo {
  flex: 0 0 auto;
  width: auto;
  height: 32px;
}

.uep-subscribe__name { display: block; }

@media (prefers-reduced-motion: reduce) {
  .uep-subscribe__card,
  .uep-subscribe__card:hover,
  .uep-subscribe__card:focus-visible {
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   MARQUEE — episode collage  (.uep-marquee)
   Appended block. Nothing above this line was reflowed.

   ⚠ NAME vs MECHANISM. This component is called .uep-marquee because that is
   the agreed component name, but it is NOT a marquee and must not be
   "fixed" into one. The stevenbartlett.com `.doac-section` it reproduces has
   no continuous motion at all. That was measured, not assumed:

     - its theme CSS carries 46 `doac` rules and not one declares `animation`,
       `animation-name` or `transition`;
     - no @keyframes in the theme is a marquee/ticker/scroll cycle;
     - its four rows are NOT duplicated content — 24 tiles drawn from 15
       unique images in a hand-picked order, so there is no repeating cycle
       for a marquee to loop over;
     - held at a fixed scroll position for 2.29s every row moved 0px.

   The motion is simpleParallax.js, declared inline on their homepage:
       new simpleParallax(getElementsByClassName('doac-img'),  {orientation:'right', scale:1.3})
       new simpleParallax(getElementsByClassName('doac-img1'), {orientation:'left',  scale:1.3})
   i.e. a MIRRORED, SCROLL-LINKED drift — a pure function of scroll position
   with no time component. That is what --row--a / --row--b reproduce below,
   in CSS, via a view() scroll timeline and no JavaScript.

   Two consequences worth knowing before you edit:
     1. At rest the section is perfectly still. That is correct.
     2. Where `animation-timeline: view()` is unsupported the tiles simply sit
        at their mid position. That is also correct — it is what the reference
        looks like whenever the user is not scrolling.

   WHAT MOVES, AND WHY IT IS THE IMAGE ITSELF. In the reference the <img> IS
   the tile: `.doac-img` carries the width, height, margin, 20px radius, 1px
   border, black background and object-fit, and its scroll-derived transform
   is `matrix(1,0,0,1,±30,0)` — a PURE TRANSLATE with no scale. So the whole
   rounded tile slides bodily and the ROW clips it, and rows 1/3 counter-slide
   against 2/4. An earlier pass here nailed a wrapper frame in place and panned
   a scale(1.2) image inside it, which reads as a Ken Burns pan behind static
   rectangles rather than a counter-sliding collage — and the 1.2 cropped every
   thumbnail 20% tighter than object-fit already does, chopping the burnt-in
   YouTube titles mid-word. There is no wrapper now. Do not reintroduce one,
   and do not add a scale: the slack the drift travels through is the row's
   own clip, exactly as it is in the reference.

   MEASURED VALUES this block reproduces (their breakpoints 768/992 remapped
   onto ours, 768/1024 — we have no 992 or 1200 step):
     tile frame   160x108  / 220x147  / 300x200      (margin 10px)
     tile radius  20px at ALL THREE tiers — confirmed live at base and via
                  CSSOM at >=992; their wide-tier block has no radius override.
                  --radius-lg (16) is 20% under it and --radius-xl (24) is 20%
                  over, so this carries its own --uep-marquee-radius.
     row height   128px    / 167px    / 220px
     rows offset  row--a left:-100px, row--b left:-250px
     rows inset   280px / 300px / 100px (as padding-top — see the note below)
     drift        +/-30px at the widest tier, mirrored per row type

   TRACK LENGTH IS A REQUIREMENT, NOT A CONTENT DECISION. Nine tiles per track
   is the minimum that covers a 2560px viewport: 9 x 320 = 2880px of track,
   less row--b's -250px offset and less the -30px drift, reaches 2600px. Six
   tiles (the reference's count) reaches only 1670px on row--b, which leaves a
   250px strip of bare ink down the right of half the rows on any 1920-wide
   monitor. The reference inherits that limitation; we are not obliged to.
   If you change the tile width or the offsets, re-derive the count.

   OVERFLOW: `overflow: hidden` then `overflow: clip` on all three nesting
   levels. Two reasons, both load-bearing. (a) `hidden` is the fallback for
   engines that do not know `clip`, so the tracks are always contained and the
   page can never scroll sideways. (b) `hidden` creates a SCROLL CONTAINER,
   and `view()` resolves against the nearest scroll container — with `hidden`
   winning, every tile's timeline would be pinned to a box that never scrolls
   and the drift would be frozen. `clip` does not create one, so the timeline
   correctly resolves against the document.
   ========================================================================== */

.uep-marquee {
  position: relative;
  /* svh, not vh: on iOS vh is the height with the URL bar HIDDEN, so a 100vh
     section overflows the visible area on first paint.
     min-height rather than a fixed height: the tile rows shortened when they
     went from 3:2 to 16:9, and a locked 100svh kept paying out the difference
     as dead ink under the org bar. This still fills the viewport when the
     content is shorter, and stops leaving a gap when it is not. */
  min-height: 100svh;
  height: auto;
  display: flex;
  flex-direction: column;
  /* flex-start + `margin-top: auto` on the rows, NOT `center`. When the
     content is shorter than 100svh, centring pays the leftover height out
     equally top and bottom — and the bottom half reads as dead ink under the
     org bar, because there is nothing below the logos to justify it. Pushing
     the whole slack above the collage instead hides it in the inset the
     section already has, and padding-bottom then sets the space under the
     logos as a deliberate number rather than whatever the arithmetic left
     over. Margins never collapse in a flex container, so the `margin-top`
     caveat noted on padding-top below does not apply to this one. */
  justify-content: flex-start;
  padding-bottom: var(--space-8);
  text-align: center;
  background: var(--uep-ink);
  color: var(--uep-on-dark);
  /* This is a new dark ground, so it owns its focus-ring colour rather than
     relying on the .uep-section--ink list in tokens.css. */
  --uep-ring: var(--uep-on-dark);
  --uep-marquee-amp: 16px;
  /* 20px at every tier — see the radius note above. Component-scoped rather
     than a site token because it exists to match one measured reference and
     sits between --radius-lg and --radius-xl on purpose. */
  --uep-marquee-radius: 20px;
  overflow: hidden;
  overflow: clip;
  /* Contain the component's internal stacking (wash 1, vignettes 10, copy 11)
     so it can never interleave with the sticky header at z-index 40 or with a
     neighbouring section. */
  isolation: isolate;
  /* The collage's inset from the top of the section. This is PADDING on the
     section and not `margin-top` on .uep-marquee__rows, which is what the
     reference uses, because of a direct consequence of the `overflow: clip`
     above: unlike `hidden`, `clip` does NOT establish a block formatting
     context, so a margin-top on the first in-flow child collapses straight
     out through the section and positions nothing — measured, the rows landed
     at offset 0 with a 300px dead band at the bottom. box-sizing is border-box
     site-wide, so this padding comes out of the 100svh rather than adding to
     it, and abspos children resolve against the padding box's top border edge,
     so .uep-marquee__content and ::before are unaffected. */
  padding-top: 280px;
}

/* Legibility wash behind the copy. The reference fades to 0.1 at the base
   tier and 0.3 above it, so this steps as well: -ink-10 below 768 and -ink-35
   from 768 up. -35 alone (which is what this used to be at every tier) runs
   3.5x denser than the reference on exactly the tier where the collage is
   smallest and has the least to give. Both stops are tokens — an undefined
   var() would silently drop the whole declaration, so check tokens.css before
   reaching for a step that is not there. Contrast is unaffected either way:
   the copy sits at top:120px, i.e. in the first ~15% of the gradient where
   the ink is still above 0.8 whichever end stop is in force. */
.uep-marquee::before {
  content: "";
  position: absolute;
  z-index: 1;
  left: 0;
  top: 40px;
  width: 100%;
  height: calc(100% - 40px);
  background: linear-gradient(var(--uep-overlay-ink-95) 0%, var(--uep-overlay-ink-10) 100%);
  pointer-events: none;
}

.uep-marquee__content {
  position: absolute;
  z-index: 11;
  left: 0;
  top: 120px;
  width: 100%;
  text-align: center;
}

/* margin-inline:auto, not the parent's text-align:center — tokens.css sets a
   global `img { display: block }`, and text-align does not centre a block. */
.uep-marquee__logo {
  width: 140px;
  height: auto;
  margin-inline: auto;
  margin-bottom: var(--space-5);
}

.uep-marquee__title {
  font-family: var(--font-headline);
  font-size: var(--fs-h4);
  font-weight: var(--fw-reg);
  line-height: var(--lh-snug);
  color: var(--uep-on-dark);
  width: 80%;
  margin-inline: auto;
}

.uep-marquee__cta {
  margin-top: var(--space-2);
}

/* ---- the collage ---- */

.uep-marquee__rows {
  position: relative;
  overflow: hidden;
  overflow: clip;
  /* Absorbs all of the section's spare height — see the justify-content note. */
  margin-top: auto;
}

/* The two vignettes that feather the collage into the section.
   These sit at z-index 10 — above the wash (1), below the copy (11) — which
   works because neither .uep-marquee__rows nor .uep-marquee itself carries a
   z-index, so the pseudos resolve in the section's own stacking context.
   `isolation: isolate` on .uep-marquee is what keeps that self-contained.

   The TOP vignette hangs off the rows, because it belongs to the top of the
   collage. The BOTTOM one hangs off the SECTION, which is a deliberate
   departure from the reference: the rows are taller than the section at wide
   tiers (880px of rows inside a 900px section, less the 100px inset, so they
   overhang by 80px), which means a rows-anchored bottom fade lands below the
   clip and never paints — the collage gets a hard scissor cut against the
   next section instead of fading out. Anchoring it to the section puts it on
   the visible bottom edge at every viewport height. */
.uep-marquee__rows::before,
.uep-marquee::after {
  content: "";
  position: absolute;
  z-index: 10;
  left: 0;
  width: 100%;
  pointer-events: none;
}
.uep-marquee__rows::before {
  top: 0;
  height: 140px;
  background: linear-gradient(var(--uep-overlay-ink-55) 10%, transparent 100%);
}
.uep-marquee::after {
  bottom: 0;
  height: 140px;
  background: linear-gradient(0deg, var(--uep-overlay-ink-95) 10%, transparent 100%);
}

.uep-marquee__row {
  position: relative;
  /* tile height + the tile's 10px margin top and bottom. Recomputed when the
     tiles moved from 3:2 to 16:9 — leaving these at the old 3:2 values left a
     band of dead ink under every row. */
  height: 110px;
  overflow: hidden;
  overflow: clip;
}

/* Absolutely positioned and nowrapped, so the run of tiles is as wide as it
   needs to be and is clipped by the row rather than widening the page.
   font-size 0 kills the inter-element whitespace that inline-block tiles would
   otherwise inherit from the newlines in the markup, leaving margin:10px as
   the only spacing — which is how the reference spaces them (it has no gap
   property anywhere). */
.uep-marquee__track {
  position: absolute;
  top: 0;
  white-space: nowrap;
  font-size: 0;
}
.uep-marquee__row--a .uep-marquee__track { left: -100px; }
.uep-marquee__row--b .uep-marquee__track { left: -250px; }

/* The <img> IS the tile — no wrapper, exactly as `.doac-img` is in the
   reference. `display: inline-block` overrides the global `img { display:
   block }` in tokens.css so the run of tiles stays on one nowrapped line; the
   reference uses `display: inline`, which for a replaced element lays out the
   same but is easier to knock over. object-fit crops the 16:9 source into the
   3:2 frame, about 11% off the width, and that is the ONLY crop applied. */
/* 16:9, matching the source artwork. These are YouTube frames and episode
   stills, all natively 16:9 — a 3:2 tile cropped ~11% off every one of them
   for no reason. Heights below are width x 9/16 at each breakpoint. */
.uep-marquee__tile {
  display: inline-block;
  width: 160px;
  height: 90px;
  margin: 10px;
  object-fit: cover;
  border: 1px solid var(--uep-overlay-white-25);
  border-radius: var(--uep-marquee-radius);
  background: var(--uep-ink);
  vertical-align: middle;
}

/* Pure translate, no scale. The tile slides bodily and .uep-marquee__row's
   `overflow: clip` cuts it off at the row edge — which is what makes rows 1/3
   read as counter-sliding against 2/4 rather than as images panning inside
   fixed frames. */
@keyframes uep-marquee-drift-a {
  from { transform: translate3d(calc(var(--uep-marquee-amp) * -1), 0, 0); }
  to   { transform: translate3d(var(--uep-marquee-amp), 0, 0); }
}
@keyframes uep-marquee-drift-b {
  from { transform: translate3d(var(--uep-marquee-amp), 0, 0); }
  to   { transform: translate3d(calc(var(--uep-marquee-amp) * -1), 0, 0); }
}

/* Opt IN behind no-preference, so a reduce user never picks the animation up
   in the first place; the explicit reduce block further down is the belt to
   this brace. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .uep-marquee__row--a .uep-marquee__tile,
    .uep-marquee__row--b .uep-marquee__tile {
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: view();
      /* THE SPEED LIVES HERE AND NOWHERE ELSE, and it is worth understanding
         before touching it, because a range percentage cannot reproduce the
         reference exactly at every window size.

         The reference's drift is an absolute function of scroll — 0.0425px of
         travel per px scrolled (research §4), the same number at every viewport
         height. Ours is a proportion of the view range, so its rate is

             rate = 2*amp / (rangeLength * (viewportHeight + tileHeight))

         and it therefore gets slower as the window gets taller. Only one height
         can be exact; the job is to centre the error across realistic ones.

         MEASURED, not assumed. At 1440x757 (amp 30, tile 200) the cover length
         reads 957px — recovered by sampling a ViewTimeline's currentTime at two
         known scroll offsets, 32.079% and 73.877% across 400px, which is the
         one way to get this number in a background tab: Chrome does not SAMPLE
         scroll-driven animations while document.visibilityState is "hidden", so
         reading a computed transform there reports the base value and looks
         like the drift is dead. It is not. The formula above then predicts
         0.0392 at 160% of cover, and 0.0392 is exactly what the timeline gives.

         Across 700-1000px of viewport height:
             140%  +12% .. -16%, exact at 807     <- best centred, and this
             148%   +6% .. -21%, exact at 757
             160%   -2% .. -26%, exact at ~690
         A review reported 140% measuring 0.047 at 1440x807 and asked for 160%.
         That does not reproduce: the geometry above puts 140% at 0.0426 there,
         i.e. dead on. 160% would run 12% SLOW at that height, not fast. Keeping
         140%.

         If you change the amplitude or the tile height, re-derive from the
         formula and then confirm against a live ViewTimeline reading. Do not
         eyeball it, and do not trust a computed transform in a hidden tab. */
      animation-range: cover -20% cover 120%;
    }
    .uep-marquee__row--a .uep-marquee__tile { animation-name: uep-marquee-drift-a; }
    .uep-marquee__row--b .uep-marquee__tile { animation-name: uep-marquee-drift-b; }
  }
}

@media (min-width: 768px) {
  .uep-marquee { --uep-marquee-amp: 22px; padding-top: 300px; }
  .uep-marquee::before {
    top: 0;
    height: 100%;
    /* 0.3 in the reference from 768 up; -ink-35 is the nearest step we carry. */
    background: linear-gradient(var(--uep-overlay-ink-95) 0%, var(--uep-overlay-ink-35) 100%);
  }
  .uep-marquee__logo { width: 180px; }
  .uep-marquee__title {
    width: 100%;
    max-width: var(--max-w-body);
    padding-inline: var(--space-5);
  }
  .uep-marquee__cta { margin-top: var(--space-4); }
  .uep-marquee__rows::before { height: 70px; background: linear-gradient(var(--uep-overlay-ink-95) 10%, transparent 100%); }
  .uep-marquee::after        { height: 70px; }
  .uep-marquee__row { height: 144px; }
  .uep-marquee__tile { width: 220px; height: 124px; }
}

@media (min-width: 1024px) {
  .uep-marquee { --uep-marquee-amp: 30px; padding-top: 100px; }
  .uep-marquee__logo { width: 220px; margin-bottom: var(--space-6); }
  .uep-marquee__title { font-size: var(--fs-h3); }
  .uep-marquee__cta { min-width: 200px; }
  .uep-marquee__rows::before { height: 180px; }
  .uep-marquee::after        { height: 280px; }
  .uep-marquee__row { height: 189px; }
  /* No border-radius step here: the reference is 20px at every tier. */
  .uep-marquee__tile { width: 300px; height: 169px; }
}

/* A permanently drifting full-viewport field is a vestibular trigger, and this
   is the one place on the site where that genuinely matters. Stop it dead —
   the tiles hold their mid position, which is what the section looks like at
   rest anyway. The reference has no such guard; ours does. */
@media (prefers-reduced-motion: reduce) {
  .uep-marquee__tile {
    animation: none;
    transform: none;
  }
}

/* -------- Guest organisation bar --------
   Lives in the dead space under the marquee tile rows. Prototyped as variant C
   on /lab/.

   NOT an "as seen on" bar. These are the companies our guests run — no outlet
   has ever covered this show. The label describes the PEOPLE ("From Leading Industry
   Voices & Operators"), which is a claim about their standing and makes no
   assertion that any of them featured or endorsed us.

   The single-colour trick: brightness(0) crushes any mark to pure black
   whatever its own colours, invert(1) flips that to white. Twelve logos in
   magenta, orange, amber, green and black then read as one strip rather than a
   ransom note. Opacity does the rest.

   The track is duplicated and the animation translates exactly -50%, so at the
   instant copy one wraps, copy two is already in its place. Any other distance
   shows a seam. */
.uep-orgbar {
  position: relative;
  /* 11, above the section's bottom vignette at 10 — NOT 1. That vignette is a
     140px band fading up from --uep-overlay-ink-95, and it exists to feather
     the COLLAGE into the next section. The org bar lives inside the same
     140px, so at z-index 1 it was being painted through ~45% ink at the base
     of the marks and ~2% at the top: the strip read dull, and unevenly dull
     down each logo. The marks are not tiles and should not be feathered. */
  z-index: 11;
  width: 100%;
  max-width: var(--max-w-wide);
  margin-inline: auto;
  padding: var(--space-7) var(--space-5) var(--space-4);
}
.uep-orgbar__label {
  margin: 0 0 var(--space-6);
  padding-top: var(--space-3);
  text-align: center;
  font-family: var(--font-headline);
  font-size: var(--fs-overline);
  font-weight: var(--fw-bld);
  letter-spacing: var(--tracking-xwide);
  text-transform: uppercase;
  color: var(--uep-on-dark-muted);
}
.uep-orgbar__viewport {
  overflow: hidden;
  /* Fade both ends so marks enter and leave rather than being guillotined. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.uep-orgbar__move {
  display: flex;
  width: max-content;
  animation: uep-orgbar-scroll 78s linear infinite;
}
.uep-orgbar__viewport:hover .uep-orgbar__move,
.uep-orgbar__viewport.is-scrolling .uep-orgbar__move { animation-play-state: paused; }
.uep-orgbar__track {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  margin: 0;
  padding: 0 var(--space-8) 0 0;
  list-style: none;
}
/* OPTICAL sizing, per mark — ported from variant C2 on /lab/.
   Each <li> carries its own --h, derived as 71.5 / aspect^0.35, which splits
   the difference between equal height and equal area. Equal height (what this
   used to do) starves a compact lockup like Bagnato; equal area shrinks a wide
   wordmark until the type is unreadable. Fixing either one alone would not
   have fixed the other.
   max-width is deliberately none: a clamp is what was crushing Srama. */
.uep-orgbar__track img {
  width: auto;
  height: var(--h, 42px);
  max-width: none;
  object-fit: contain;
  filter: brightness(0) invert(1);
  opacity: 0.72;
  transition: opacity var(--dur-med) var(--ease-out);
}
.uep-orgbar__track img:hover { opacity: 1; }
@media (max-width: 700px) {
  /* 0.55, not 0.7. These marks are wordmarks, so their WIDTH is what decides
     how many fit — "HENDERSON." alone is ~150px of a 323px strip at 0.7. The
     optical --h ratios between marks are preserved, the whole set just sits
     smaller so three or four read at once instead of two. */
  .uep-orgbar__track img { height: calc(var(--h, 42px) * 0.55); }
  /* Gap was --space-7 (48px), which is a seventh of a 323px strip. At that
     spacing only TWO of the twelve marks were on screen at once — measured —
     so the bar read as a near-empty band that occasionally slid a logo past.
     Tighter here only; desktop keeps the generous rhythm. */
  .uep-orgbar__track { gap: var(--space-5); }
  /* The 10%/90% fade costs ~32px at each end of a phone-width strip, which is
     most of a logo on both sides. Narrowed so the marks are legible right up
     to the edges and more of them are in the clear zone at any moment. */
  .uep-orgbar__viewport {
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 4%, #000 96%, transparent);
            mask-image: linear-gradient(90deg, transparent, #000 4%, #000 96%, transparent);
    /* Swipeable on touch, where there is no hover to pause with. The loop keeps
       running; initOrgBar() pauses it while a finger is on the strip and starts
       it again once the visitor stops, so a manual scroll is never fighting the
       animation for the same pixels. */
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  /* DEFECT FIX: below 700 the strip is swipeable, and the CSS transform loop
     fought it. transform and scrollLeft are different coordinate spaces — the
     animation slid the track left while a swipe moved the scroll port right, so
     a visitor could land past the duplicated track looking at empty strip. That
     is the "logos sometimes disappear" report.
     The transform is switched off here and initOrgBar() drives scrollLeft
     instead, so the loop and the finger move the same number. */
  .uep-orgbar__move { animation: none; transform: none; }
  .uep-orgbar__viewport::-webkit-scrollbar { display: none; }
}
@keyframes uep-orgbar-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .uep-orgbar__move { animation: none; }
  .uep-orgbar__viewport { overflow-x: auto; }
}

/* ==========================================================================
   TEAM GRID  (.uep-team)  —  "Who is Unemployable"
   --------------------------------------------------------------------------
   Four square portraits in a card grid, each with a role line, a one-line
   descriptor and an expandable bio. Promoted from variant B on /lab2/.

   ⚠ HEADINGS ON GREEN. .uep-section--green sets `h1, h2, h3 { color: white }`
   so that section headings survive the green ground. The card names are <h3>
   INSIDE a white card, so they take that white and render white-on-white
   unless they are overridden. `.uep-team__card .uep-team__name` below is that
   override. It needs BOTH classes: at one class it is (0,1,0) and loses to
   `.uep-section--green h3` at (0,1,1). Weakening that selector makes every
   name vanish, and it vanishes silently.

   ⚠ SQUARE CROPS. The three original portraits are 1080x1350 (4:5), so a
   square drops 270px. Every face sits in the upper half of its frame, so the
   crop is biased to the top per person via --uep-team-pos: Carson 30%,
   Shannon 30%, Eric 0% (hard top — his cap reads as clipped at anything
   lower), Assem 50% (his source is already 1:1, so nothing is cropped and the
   value is inert). A centred crop clips Eric's cap and leaves dead shirt
   under all three of the 4:5 files.
   ========================================================================== */

.uep-team {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}
@media (min-width: 640px)  { .uep-team { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .uep-team { grid-template-columns: repeat(4, 1fr); gap: var(--space-4); } }
/* Five-up. auto-fit rather than repeat(5, 1fr) so it degrades on its own: at
   1024 five 220px cards plus gaps overflow, and the grid drops to four rather
   than squeezing five into unreadable columns. With five items auto-fit can
   never produce more than five tracks, so there is no runaway at wide
   viewports either. */
@media (min-width: 1024px) {
  .uep-team--5 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
}

/* Three-up. The base rule is four-up because four of the five pages that use
   this grid still carry four cards. The home page dropped to three when Assem
   was removed from the coaches listing, and the shared rule left it rendering
   three cards in a four-track grid -- a full empty column, about a quarter of
   the row bare down the right-hand side. Modifier rather than a change to the
   base rule, for the same reason .uep-team--5 exists: the other pages must not
   move. At 640-1023px the base two-up leaves the third card alone on its own
   row, which is the normal way an odd count wraps and is left as is. */
@media (min-width: 1024px) {
  .uep-team--3 { grid-template-columns: repeat(3, 1fr); }
}

.uep-team__card {
  background: var(--uep-surface);
  border-radius: var(--radius-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* No border. On the green ground a hairline reads as a seam; the white card
     already separates itself from the background by luminance alone. */
  box-shadow: 0 12px 32px rgba(12, 19, 32, 0.14);
}

.uep-team__media {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--uep-navy-deep);
}
.uep-team__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% var(--uep-team-pos, 30%);
  display: block;
}

/* Assem has no photograph yet. A monogram rather than a grey box or a stock
   silhouette: it reads as a designed state, and it does not put a stranger's
   face beside a real name. When the photo lands, replace this whole block
   with an <img> and give it a --uep-team-pos. */
.uep-team__mono {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: radial-gradient(120% 120% at 30% 20%, var(--uep-navy) 0%, var(--uep-ink) 70%);
}
.uep-team__initials {
  font-family: var(--font-headline);
  font-weight: 800;
  font-size: clamp(2rem, 7vw, 3.25rem);
  letter-spacing: var(--tracking-tight);
  color: var(--uep-green-light);
  line-height: 1;
}
.uep-team__pending {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: var(--space-3);
  text-align: center;
  font-family: var(--font-headline);
  font-weight: 600;
  font-size: 0.65rem;
  letter-spacing: var(--tracking-xwide);
  text-transform: uppercase;
  color: var(--uep-on-dark-faint);
}

.uep-team__body { padding: var(--space-5); }

/* Scoped to the card, and that is load-bearing rather than tidy: the plain
   `.uep-team__name` selector is specificity (0,1,0) and LOSES to
   `.uep-section--green h3` at (0,1,1), which is how the names first shipped
   white-on-white. Two classes beats one-class-one-type, so this holds on any
   section modifier — green, ink or alt — instead of only the one it was
   tested against. */
.uep-team__card .uep-team__name {
  color: var(--uep-ink);
  font-family: var(--font-headline);
  font-weight: 800;
  font-size: 1.3125rem;
  line-height: 1.15;
  letter-spacing: var(--tracking-tight);
  margin: 0 0 4px;
}
.uep-team__role {
  font-family: var(--font-headline);
  font-weight: 600;
  font-size: 0.7rem;
  letter-spacing: var(--tracking-xwide);
  text-transform: uppercase;
  color: var(--uep-green);
  margin: 0 0 var(--space-3);
}
.uep-team__tag {
  font-weight: 500;
  font-style: italic;
  color: var(--uep-ink);
  margin: 0 0 var(--space-3);
}
.uep-team__bio {
  color: var(--uep-slate);
  font-size: 0.95rem;
  line-height: 1.62;
  margin: 0 0 var(--space-3);
}
.uep-team__bio:last-child { margin-bottom: 0; }

/* Expandable bio on a native <details>: no JavaScript, keyboard-operable,
   and the closed text is still found by the browser's in-page search. */
.uep-team__more { margin-top: var(--space-2); }
.uep-team__more > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-headline);
  font-weight: 600;
  font-size: 0.8rem;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--uep-green);
}
.uep-team__more > summary::-webkit-details-marker { display: none; }
.uep-team__more > summary::after {
  content: "";
  width: 7px; height: 7px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur-fast) var(--ease-out);
}
.uep-team__more[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.uep-team__more > summary:hover { color: var(--uep-green-darker); }
.uep-team__more-body { padding-top: var(--space-3); }

/* Section lede on the green ground. --uep-on-dark-muted is a cool grey tuned
   for the ink sections and goes muddy on green, so this is plain white held
   back on alpha instead. */
.uep-team-lede {
  color: var(--uep-overlay-white-90);
  max-width: 56ch;
  /* The gap down to the cards lives here rather than on a utility class:
     there is no .uep-mb-7 in this kit — .uep-mb-0 is the only margin utility
     that exists, and a class that resolves to nothing fails silently. */
  margin-bottom: var(--space-7);
}

/* ==========================================================================
   VARIANT B — six-tile grid (.uep-tiles)
   --------------------------------------------------------------------------
   Two rows of three, full-bleed, tiles touching. Each is a still under a dark
   wash with a long arrow above a hard uppercase title in the bottom-left
   corner, and each plays a short clip on hover or keyboard focus.
   Promoted from variant C on /lab2/.

   The shape is modelled on a reference site's tile grid. Everything IN it is
   ours — our six destinations, our footage, our palette, our typeface. None of
   the reference's copy, product names or imagery appears here; those are
   someone else's trademarks.

   ⚠ TYPEFACE. The reference sets its titles in a CONDENSED grotesque. There
   is no condensed face in this kit, so these are Poppins 800 uppercase with
   the tracking pulled in as far as it goes. They read wider and always will —
   closing that gap means licensing a condensed face, not editing CSS.
   ========================================================================== */

.uep-tiles {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  background: var(--uep-ink);
}
@media (min-width: 640px)  { .uep-tiles { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .uep-tiles { grid-template-columns: repeat(3, 1fr); } }

.uep-tiles__tile {
  position: relative;
  display: block;
  overflow: hidden;
  text-decoration: none;
  color: var(--uep-on-dark);
  isolation: isolate;
  /* A shared height rather than an aspect-ratio, for the same reason as the
     band above: three aspect-ratio tiles in a 1fr grid disagree with their
     track when the viewport does not divide by three, and the remainder shows
     as a seam. The reference tiles run about 3:2; 22vw lands there at common
     widths and stops growing on an ultrawide. */
  height: clamp(260px, 22vw, 460px);
}

.uep-tiles__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--uep-tiles-pos, 50% 50%);
  transform: scale(1);
  transition: transform var(--dur-slow) var(--ease-out);
  z-index: 0;
}

/* Two layers, not one. A flat wash alone either kills the photograph or fails
   to hold the type; the flat layer lifts the whole tile off white imagery and
   the gradient does the real legibility work in the corner where the lockup
   sits. */
.uep-tiles__wash {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(to top, var(--uep-overlay-ink-95) 0%, rgba(12, 19, 32, 0.15) 55%, rgba(12, 19, 32, 0) 80%),
    var(--uep-overlay-ink-35);
  transition: opacity var(--dur-med) var(--ease-out);
}

.uep-tiles__lockup {
  position: absolute;
  z-index: 2;
  left: 0; bottom: 0;
  padding: var(--space-6) var(--space-5);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  transition: transform var(--dur-med) var(--ease-out);
}

.uep-tiles__arrow {
  display: block;
  width: 42px;
  height: auto;
  color: var(--uep-on-dark);
  transition: transform var(--dur-med) var(--ease-out), color var(--dur-med) var(--ease-out);
}

/* Second line picked out in gold. A <span> inside the title rather than a
   separate element, so it stays part of the same text node for wrapping,
   selection and screen readers. */
.uep-tiles__title-accent { color: var(--uep-gold); }

.uep-tiles__title {
  /* Named colour: tokens.css sets `h1..h6 { color: ink }` and these sit on a
     photograph. Inheriting would render them ink-on-image. */
  color: var(--uep-on-dark);
  font-family: var(--font-headline);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.5rem, 2.1vw, 2.125rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  margin: 0;
  text-wrap: balance;
}

@media (hover: hover) {
  .uep-tiles__tile:hover .uep-tiles__img { transform: scale(1.06); }
  .uep-tiles__tile:hover .uep-tiles__wash { opacity: 0.72; }
  .uep-tiles__tile:hover .uep-tiles__lockup { transform: translateY(-8px); }
  .uep-tiles__tile:hover .uep-tiles__arrow { transform: translateX(10px); color: var(--uep-gold); }
}
.uep-tiles__tile:focus-visible { outline: 3px solid var(--uep-gold); outline-offset: -3px; }
.uep-tiles__tile:focus-visible .uep-tiles__img { transform: scale(1.06); }
.uep-tiles__tile:focus-visible .uep-tiles__wash { opacity: 0.72; }
.uep-tiles__tile:focus-visible .uep-tiles__lockup { transform: translateY(-8px); }
.uep-tiles__tile:focus-visible .uep-tiles__arrow { transform: translateX(10px); color: var(--uep-gold); }

@media (prefers-reduced-motion: reduce) {
  .uep-tiles__img, .uep-tiles__wash, .uep-tiles__lockup, .uep-tiles__arrow { transition: none; }
  .uep-tiles__tile:hover .uep-tiles__img,
  .uep-tiles__tile:focus-visible .uep-tiles__img,
  .uep-tiles__tile:hover .uep-tiles__lockup,
  .uep-tiles__tile:focus-visible .uep-tiles__lockup,
  .uep-tiles__tile:hover .uep-tiles__arrow,
  .uep-tiles__tile:focus-visible .uep-tiles__arrow { transform: none; }
}

/* --- hover video ----------------------------------------------------------
   The reference plays footage in each tile on hover. The poster image stays
   underneath and the <video> fades in over it, so there is never a blank
   frame while the clip buffers — a video that has not started yet is simply
   transparent and the still shows through.

   preload="none": six autoloading clips would cost ~1.2MB on page load for
   footage most visitors never trigger. They fetch on first hover instead.

   The JS half is initTileVideo() in app.js. It has to: a muted autoplay attribute would
   start all six at once, and `play()` returns a promise that rejects if the
   pointer leaves before the clip is ready — unhandled, that logs an error on
   every fast mouse-out. */
.uep-tiles__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--uep-tiles-pos, 50% 50%);
  opacity: 0;
  transition: opacity var(--dur-med) var(--ease-out);
  z-index: 0;         /* same layer as the poster, above it in source order */
  pointer-events: none;
}
.uep-tiles__tile.is-playing .uep-tiles__video { opacity: 1; }

/* With the clip running, the still underneath does not need to zoom too —
   holding it steady stops a double-motion judder as the video fades in. */
.uep-tiles__tile.is-playing .uep-tiles__img { transform: scale(1.06); }

@media (prefers-reduced-motion: reduce) {
  /* No autoplay-on-hover at all: motion is the thing being opted out of.
     The JS checks the same query before calling play(). */
  .uep-tiles__video { transition: none; }
  .uep-tiles__tile.is-playing .uep-tiles__video { opacity: 0; }
}

/* ==========================================================================
   MEMBER LOGIN MODAL  (.uep-authmodal)
   --------------------------------------------------------------------------
   A native <dialog> opened with showModal(). That choice does real work: it
   supplies the focus trap, the Escape handler, ::backdrop, and inert-ing of
   the page behind — four things a div-based modal reliably gets wrong.

   Ink panel so it reads as part of the site's dark surfaces rather than a
   browser artefact, with the green button as the single obvious action and
   the gold one below the rule as the secondary path.
   ========================================================================== */

.uep-authmodal {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--uep-on-dark);
  max-width: min(440px, calc(100vw - 2 * var(--space-5)));
  width: 100%;
  /* A dialog is display:none until open, so this only ever applies when
     showModal() has run; there is no hidden-but-laid-out state to fight. */
  overflow: visible;
}
.uep-authmodal::backdrop {
  background: rgba(12, 19, 32, 0.72);
  backdrop-filter: blur(2px);
}

.uep-authmodal__panel {
  position: relative;
  background: var(--uep-ink);
  border: 1px solid var(--uep-overlay-white-10);
  border-radius: var(--radius-lg);
  padding: var(--space-7) var(--space-6) var(--space-6);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
  text-align: left;
}

.uep-authmodal__close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--uep-on-dark-muted);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}
.uep-authmodal__close:hover { background: var(--uep-overlay-white-08); color: var(--uep-on-dark); }

/* Named colour, not inherited: tokens.css sets `h1..h6 { color: ink }`, which
   beats a colour on the container and would render this ink-on-ink. */
.uep-authmodal__title {
  color: var(--uep-on-dark);
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  font-size: var(--fs-h3);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-3);
  padding-right: var(--space-7);          /* clears the close button */
}
.uep-authmodal__lede {
  color: var(--uep-on-dark-muted);
  font-size: var(--fs-small);
  margin: 0 0 var(--space-5);
}

.uep-authmodal__form { margin: 0; }
.uep-authmodal__label {
  display: block;
  font-family: var(--font-headline);
  font-weight: var(--fw-sbd);
  font-size: var(--fs-overline);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--uep-on-dark-muted);
  margin-bottom: var(--space-2);
}
.uep-authmodal__input {
  width: 100%;
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--uep-overlay-white-18);
  background: var(--uep-overlay-white-04);
  color: var(--uep-on-dark);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  margin-bottom: var(--space-4);
}
.uep-authmodal__input::placeholder { color: var(--uep-on-dark-faint); }
.uep-authmodal__input:focus-visible {
  outline: 2px solid var(--uep-green-light);
  outline-offset: 2px;
  border-color: transparent;
}
.uep-authmodal__input[aria-invalid="true"] { border-color: var(--uep-danger); }

.uep-authmodal__error {
  color: #ffb4b1;
  font-size: var(--fs-small);
  margin: calc(var(--space-4) * -1) 0 var(--space-4);
}
.uep-authmodal__sent {
  color: var(--uep-green-light);
  font-size: var(--fs-small);
  margin: 0 0 var(--space-5);
}
.uep-authmodal__sent strong { color: var(--uep-on-dark); overflow-wrap: anywhere; }

/* All white, unlike the green success message: this is not an error the
   visitor caused, so it is stated plainly rather than coloured as a warning.
   The link carries the weight instead. */
.uep-authmodal__blocked {
  color: var(--uep-on-dark);
  font-size: var(--fs-small);
  line-height: var(--lh-normal);
  margin: 0 0 var(--space-5);
}
.uep-authmodal__fallback {
  color: var(--uep-gold);
  font-weight: var(--fw-bld);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.uep-authmodal__fallback:hover { color: var(--uep-gold-dark); }
.uep-authmodal__blocked strong { color: var(--uep-on-dark); overflow-wrap: anywhere; }

/* Retry. White and quiet on purpose: it is the escape hatch from the success
   state, not a second call to action competing with it. */
.uep-authmodal__retry {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--uep-on-dark);
  font-family: var(--font-headline);
  font-weight: var(--fw-sbd);
  font-size: var(--fs-small);
  cursor: pointer;
}
.uep-authmodal__retry:hover { text-decoration: underline; }
.uep-authmodal__retry:focus-visible { outline: 2px solid var(--uep-gold); outline-offset: 3px; }
.uep-authmodal__retry svg { flex: none; }

/* The line that sits BETWEEN the two soft-sell buttons. It supplies the gap
   on both sides, so neither button needs its own spacing rule — .__btn2 is
   gone for that reason rather than left behind as a no-op class. */
/* Scoped to .__foot, and that is load-bearing rather than tidy. The element
   carries BOTH .__footcopy and .__footcopy--between; at one class each they
   tie on specificity and the LATER rule wins — and .__footcopy is defined
   further down this file, so it silently took the margin back. text-align
   survived only because the base rule does not declare it, which is exactly
   the kind of half-applied rule that reads as "the CSS didn't work". Two
   classes beats one, so this holds wherever either rule ends up. */
.uep-authmodal__foot .uep-authmodal__footcopy--between {
  /* More air above than below: the line belongs to the button underneath it,
     rather than floating equidistant between the two. */
  margin: var(--space-6) 0 var(--space-3);
  text-align: center;
}
.uep-authmodal__footcopy--between strong { color: var(--uep-on-dark); }

/* The two soft-sell buttons are smaller than the site's default .uep-btn.
   Scoped to .__foot with two classes so it beats the base rule regardless of
   where either sits in this file — the same tie that quietly ate the margin
   above. Log In is deliberately left at full size: it is the reason the modal
   exists, and three identical buttons gave it no more weight than the two
   below the divider. */
.uep-authmodal__foot .uep-btn {
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  font-size: var(--fs-small);
}

.uep-authmodal__rule {
  border: 0;
  border-top: 1px solid var(--uep-overlay-white-10);
  margin: var(--space-6) 0 var(--space-5);
}

.uep-authmodal__footlede {
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  color: var(--uep-on-dark);
  font-size: var(--fs-body);
  margin: 0 0 var(--space-2);
}
.uep-authmodal__footcopy {
  color: var(--uep-on-dark-muted);
  font-size: var(--fs-small);
  margin: 0 0 var(--space-4);
}

/* ==========================================================================
   SHOW HERO  (.uep-showhero)  —  two shows, unequal split
   --------------------------------------------------------------------------
   Two full-bleed blocks that anchor to the sections below them. The split is
   40/60 rather than even: both titles are set at the same size, so the
   hierarchy between the two shows is carried by COLUMN WIDTH alone. Promoted
   from Hero 2 on /lab2/.

   The blocks take a shared height rather than an aspect-ratio. Two
   aspect-ratio blocks in a 1fr grid disagree with their track at widths that
   do not divide evenly, and the remainder shows as a seam between them.
   ========================================================================== */
.uep-showhero__kicker {
  font-family: var(--font-headline); font-weight: 700; font-size: var(--fs-overline);
  letter-spacing: var(--tracking-xwide); text-transform: uppercase; color: var(--uep-gold);
}
.uep-showhero__title {
  color: var(--uep-on-dark); font-family: var(--font-headline); font-weight: 800;
  text-transform: uppercase; line-height: 1.02; letter-spacing: -0.02em;
}
.uep-showhero__cue {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-headline); font-weight: var(--fw-sbd); font-size: var(--fs-small);
  letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--uep-on-dark);
}
.uep-showhero__cue::after {
  content: ""; width: 8px; height: 8px;
  border-top: 2px solid currentColor; border-right: 2px solid currentColor;
  transform: rotate(135deg); transition: transform var(--dur-med) var(--ease-out);
}
.uep-showhero__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1); transition: transform var(--dur-slow) var(--ease-out); z-index: 0; }
.uep-showhero__wash { position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(to top, var(--uep-overlay-ink-95) 0%, rgba(12,19,32,0.2) 58%, rgba(12,19,32,0) 85%),
              var(--uep-overlay-ink-35);
  transition: opacity var(--dur-med) var(--ease-out); }

/* --- 2 · offset split ------------------------------------------------------
   Same two blocks, unequal weight. The Show is the thing this page is being
   built to launch, so it takes the larger share and the bigger type. */
.uep-showhero { display: grid; grid-template-columns: 1fr; gap: 0; background: var(--uep-ink); }
@media (min-width: 768px) { .uep-showhero { grid-template-columns: 40fr 60fr; } }
/* Split variant (.uep-showhero--split). Two columns from 768: one block runs
   the full height of the band on the left, the other two stack in the right
   column at half its height each, so the band still ends on one clean line.

   Placement is explicit rather than source-ordered. The DOM order is the order
   of the sections these blocks anchor to, which is also the order the single
   mobile column reads in; moving the tall block to the front of the source
   would have reordered the phone to serve the desktop. The cost is that
   desktop tab order zigzags right-top → left → right-bottom, which is
   acceptable where all three blocks sit in one band above the fold.

   The half blocks get their own height rather than dividing the base clamp:
   half of 300px is 150px, which the shared title scale does not fit once the
   kicker and the cue are under it. They set the band, and the tall block
   stretches across both rows to match — hence height:auto on it, which is what
   lets the grid stretch override the base block height.

   :nth-of-type, NOT :nth-child — the section's first child is the page h1,
   which is visually-hidden and absolutely positioned, so it takes no grid cell
   but still counts as a child. Placing by :nth-child put every block one slot
   out and left the third to auto-place into whatever hole was left. */
@media (min-width: 768px) {
  /* 55/45 rather than even: the tall block carries a full-height image and
     reads narrow next to its own height, so it takes the larger share. The
     stacked pair keep their own fixed heights, so the bottoms stay level. */
  .uep-showhero--split { grid-template-columns: 55fr 45fr; }
  .uep-showhero--split > .uep-showhero__block:nth-of-type(1) { grid-column: 2; grid-row: 1; }
  .uep-showhero--split > .uep-showhero__block:nth-of-type(2) { grid-column: 1; grid-row: 1 / span 2; height: auto; }
  .uep-showhero--split > .uep-showhero__block:nth-of-type(3) { grid-column: 2; grid-row: 2; }
  .uep-showhero--split > .uep-showhero__block:nth-of-type(1),
  .uep-showhero--split > .uep-showhero__block:nth-of-type(3) { height: clamp(200px, 24vw, 300px); }
  /* The half blocks are a third of the tall one's height at the narrow end of
     the tier; the body keeps the full side padding and gives back some of the
     vertical, which is the axis that is actually short. */
  .uep-showhero--split > .uep-showhero__block:nth-of-type(1) .uep-showhero__body,
  .uep-showhero--split > .uep-showhero__block:nth-of-type(3) .uep-showhero__body { padding-block: var(--space-5); }
}
/* 2b: same block height and the same type scale, equal columns. It is not a
   duplicate of Hero 1 — there the two halves match in every respect, here the
   Show still carries the larger title inside an equal-sized block. */
@media (min-width: 768px) { .uep-showhero--even { grid-template-columns: 1fr 1fr; } }
.uep-showhero__block { position: relative; display: block; overflow: hidden; isolation: isolate;
  text-decoration: none; color: var(--uep-on-dark); height: clamp(300px, 44vw, 580px); }
.uep-showhero__body { position: absolute; z-index: 2; left: 0; bottom: 0;
  padding: var(--space-7) var(--space-6); display: flex; flex-direction: column;
  align-items: flex-start; gap: var(--space-2); transition: transform var(--dur-med) var(--ease-out); }
/* Both titles at the same size. The hierarchy between the two shows is now
   carried by the COLUMN WIDTH alone, not by the type — which is the point of
   an offset split. The --minor/--major classes stay because the block widths
   still differ; only the type scale was equalised. */
.uep-showhero__block .uep-showhero__title { font-size: clamp(1.75rem, 3.4vw, 3rem); }
@media (hover: hover) {
  .uep-showhero__block:hover .uep-showhero__img { transform: scale(1.05); }
  .uep-showhero__block:hover .uep-showhero__wash { opacity: 0.8; }
  .uep-showhero__block:hover .uep-showhero__body { transform: translateY(-6px); }
}
.uep-showhero__block:focus-visible { outline: 3px solid var(--uep-gold); outline-offset: -3px; }

/* ==========================================================================
   SHOW HEAD  (.uep-showhead)  —  two-line show name over a small label
   --------------------------------------------------------------------------
   The show's name set as a lockup rather than a sentence, with a small label
   under it pointing at whatever follows. Started life inside .uep-cine; moved
   out the moment the podcast section wanted the same treatment, because two
   copies of a type lockup drift the first time either is touched.

   Uppercase 800 to match how a show name is set in the hero. --fs-h2 tops out
   at 3rem, exactly the hero title's size, so on desktop they match — the
   section heading is distinguished by its line break and its label, not by
   scale.

   The label is --on-dark-muted, not gold. In the cinema the caption directly
   under the player is already gold, and two gold lines either side of the
   artwork put the accent on the furniture instead of on the video.

   The title sets its own colour even though both grounds it runs on
   (.uep-cine, .uep-section--ink) already colour their headings white. That is
   deliberate: those rules are h1-h6 element selectors at (0,1,1) and would
   silently outrank this class at (0,1,0) if the values ever diverged.
   ========================================================================== */
/* Lockup left, brand mark hard right. Bottom alignment rather than centre:
   the lockup is three lines deep and a vertically centred mark beside it
   floats with nothing to line up to, where sitting it on the last line puts
   it level with the label. */
.uep-showhead {
  margin-bottom: var(--space-5);
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
}
.uep-showhead__title {
  color: var(--uep-on-dark); font-family: var(--font-headline); font-weight: 800;
  font-size: var(--fs-h2); line-height: 1.02; letter-spacing: -0.02em;
  text-transform: uppercase; margin: 0 0 var(--space-2);
}
/* An optional first line for a name that splits into brand + show. Takes its
   metrics from .uep-showhero__kicker in the hero — 12px, 700, --tracking-xwide,
   uppercase — but sets WHITE rather than that class's gold, so the brand line
   reads as part of the title lockup instead of as a third accent competing
   with the gold already in play elsewhere on the page. It lives INSIDE the
   heading rather than as a sibling <p>, so the heading's accessible name is
   still the full "Unemployable Property Podcast" and the document outline
   reads correctly — only the rendering splits.

   Every property the parent .uep-showhead__title sets has to be restated
   here, including letter-spacing: the title runs -0.02em, and an overline
   inheriting negative tracking is the one thing that would give this away as
   a hand-made copy rather than the same object. */
/* Optional rule under the lockup, for a head that introduces a LIST rather
   than a single piece of media. The cinema does not take it: a rule directly
   above one large player would read as the top edge of the artwork.
   --overlay-white-10 is the same hairline the kit already uses on dark
   grounds, so it sits at the weight the rest of the page expects. */
/* The rule, and only the rule — the row layout is on the base class above,
   because both heads carry a mark now. --overlay-white-10 is the hairline the
   kit already uses on dark grounds, so it sits at the weight the rest of the
   page expects. Kept as a modifier rather than folded into the base: a head
   that opens a section with no content directly under it would be drawing a
   line across nothing.

   ONE BLOCK per selector. This was briefly two rules for the same one, which
   is how a later "remove the border" edit silently fails: the earlier
   declaration survives because the later rule never mentions the property. */
.uep-showhead--ruled {
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--uep-overlay-white-10);
}

/* Decorative in both placements — the same wordmark is already the site's home
   link in the header above, and the heading beside it names the show in text —
   so alt="" keeps it out of the accessibility tree rather than announcing the
   brand a second and third time on one page. */
.uep-showhead__mark {
  flex: none; width: 150px; height: auto; display: block;
  /* Nudged off the baseline so it does not sit hard on the rule below it. */
  margin-bottom: var(--space-1);
}
@media (max-width: 639px) {
  /* Below 640 the two would each take a full row and the mark would read as a
     stray graphic under the label. It is decoration; drop it. */
  .uep-showhead__mark { display: none; }
}

.uep-showhead__kicker {
  display: block;
  font-family: var(--font-headline); font-weight: 700; font-size: var(--fs-overline);
  letter-spacing: var(--tracking-xwide); text-transform: uppercase;
  color: var(--uep-on-dark); margin-bottom: var(--space-2);
}
.uep-showhead__label {
  font-family: var(--font-headline); font-weight: var(--fw-sbd);
  font-size: var(--fs-small); letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--uep-on-dark-muted); margin: 0;
}

/* ==========================================================================
   CINEMA  (.uep-cine)  —  one player, a scrolling episode strip
   --------------------------------------------------------------------------
   Promoted from FFD 4 on /lab2/, rule for rule. The thumbnail is a <button>,
   not an iframe: YouTube's player is ~1MB of third-party script and loading
   one per episode on arrival would be the slowest thing on the page. It
   arrives on click, and only one ever exists — opening a second tears the
   first down, because a closed-but-present iframe keeps playing.

   The caption under the player is pinned to one line of title and two of
   blurb, so switching episode cannot change the section height and shift the
   page while someone is choosing.

   The section owns its own ground and padding rather than composing with
   .uep-section--ink. That is deliberate: --ink also sets a colour on every
   h1-h6 inside it at (0,1,1), which outranks .uep-cine__title at (0,1,0) —
   the same specificity trap that has already cost this kit twice.
   ========================================================================== */
.uep-cine {
  background: var(--uep-ink);
  color: var(--uep-on-dark);
  padding-block: var(--space-9);
  /* Less underneath than above. The strip already carries its own 12px, and
     each chip another 12px below its title, so the section's own 6rem read as
     a much bigger gap under the thumbnails than the 6rem above the heading. */
  padding-bottom: var(--space-7);
}

/* --- the player ------------------------------------------------------------
   Matched to .uep-epirow__media / __thumb / __play on this same page, value
   for value: same radius, same navy ground, same 64px gold disc with the
   triangle drawn in CSS, same shadow, same 1.03 / 1.08 hover pair. */
.uep-cine__stage { margin-bottom: var(--space-5); }
.uep-cine__media {
  position: relative; display: block; width: 100%; padding: 0; border: 0;
  background: var(--uep-navy-deep); border-radius: var(--radius-md);
  overflow: hidden; cursor: pointer; aspect-ratio: 16 / 9;
}
.uep-cine__media img { width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform var(--dur-med) var(--ease-out); }
.uep-cine__play {
  position: absolute; inset: 0; margin: auto; width: 64px; height: 64px;
  border-radius: var(--radius-pill); background: var(--uep-gold);
  display: grid; place-items: center; box-shadow: var(--shadow-lg);
  pointer-events: none; transition: transform var(--dur-med) var(--ease-out);
}
.uep-cine__play::after {
  content: ""; width: 0; height: 0; border-style: solid;
  border-width: 11px 0 11px 18px;
  border-color: transparent transparent transparent var(--uep-ink);
  margin-left: var(--space-1);
}
.uep-cine__media:hover img { transform: scale(1.03); }
.uep-cine__media:hover .uep-cine__play { transform: scale(1.08); }
.uep-cine__media:focus-visible { outline: 3px solid var(--uep-gold); outline-offset: 3px; }
.uep-cine__frame { width: 100%; aspect-ratio: 16 / 9; border: 0; border-radius: var(--radius-md); display: block; }

/* --- the now-playing caption -----------------------------------------------
   The caption changes text when a different episode is picked, and two titles
   of different length would change the section's height underneath the player
   — the page jumping while you choose. So both lines are pinned: the title to
   one line with an ellipsis, the blurb to exactly two. The height is then
   identical for every episode, however long its title. */
.uep-cine__meta { margin-bottom: var(--space-6); }
.uep-cine__kicker {
  font-family: var(--font-headline); font-weight: 700; font-size: var(--fs-overline);
  letter-spacing: var(--tracking-xwide); text-transform: uppercase;
  color: var(--uep-gold); margin: 0 0 var(--space-2);
}
.uep-cine__title {
  color: var(--uep-on-dark); font-family: var(--font-headline); font-weight: 800;
  font-size: clamp(1.4rem, 2.6vw, 2.1rem); line-height: 1.1; margin: 0 0 var(--space-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* No measure on the block itself: that capped the TITLE at 62ch too, so a long
   one ellipsised well before it ran out of room. The title runs the full width
   of the player above it and only truncates at that edge; the measure moves to
   the blurb, which is the part that actually needs one. */
.uep-cine__blurb {
  color: var(--uep-on-dark-muted); font-size: var(--fs-small); margin: 0 0 var(--space-3);
  max-width: 62ch;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  /* Reserve the two lines even when the text is shorter, or a one-line blurb
     still shortens the block. */
  min-height: calc(2 * 1.62em);
}

/* --- the strip -------------------------------------------------------------
   Horizontal scroll rather than wrap: the strip is a playlist, and a playlist
   that pushes the page down every time an episode is added is not one. */
.uep-cine__strip { display: flex; gap: var(--space-4); overflow-x: auto;
  padding-bottom: var(--space-3); scroll-snap-type: x mandatory;
  scrollbar-width: none; -ms-overflow-style: none; }
.uep-cine__strip::-webkit-scrollbar { display: none; }
/* flex, not block. As a block the whitespace between <button> and <img> forms
   an anonymous line box ABOVE the image, pushing it down 5px — which is why
   the placeholder beside it sat 5px higher despite being the same size. A flex
   container discards whitespace-only children, so both start at zero. */
.uep-cine__chip {
  position: relative;
  flex: 0 0 var(--uep-cine-chip, 210px); scroll-snap-align: start;
  background: transparent; border: 0; padding: 0 0 var(--space-3);
  text-align: left; cursor: pointer; color: var(--uep-on-dark);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.uep-cine__chip img {
  width: 100%; aspect-ratio: 16/9; object-fit: cover;
  border-radius: var(--radius-sm); display: block;
  border: 2px solid transparent; transition: border-color var(--dur-fast) var(--ease-out);
}
.uep-cine__chiptitle { display: block; font-size: var(--fs-small);
  font-family: var(--font-headline); font-weight: var(--fw-sbd); line-height: 1.25; }
.uep-cine__chip:hover .uep-cine__chiptitle { color: var(--uep-gold); }
.uep-cine__chip:focus-visible { outline: 3px solid var(--uep-gold); outline-offset: 3px; }
.uep-cine__chip.is-active img { border-color: var(--uep-gold); }
.uep-cine__chip.is-active .uep-cine__chiptitle { color: var(--uep-gold); }
.uep-cine__chip.is-active::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 3px; border-radius: 2px; background: var(--uep-gold);
}

/* --- scroller and arrows ---------------------------------------------------
   The arrows sit OUTSIDE the strip: a control on top of the artwork obscures
   the thing it is there to help you choose. They hide themselves when
   everything already fits.

   --uep-cine-chip is the single source for the chip width, so the arrow offset
   below stays correct if it changes. */
.uep-cine__scroller { --uep-cine-chip: 210px;
  position: relative; display: grid; grid-template-columns: 1fr; align-items: start; }
.uep-cine__scroller.is-scrollable { grid-template-columns: auto 1fr auto; gap: var(--space-3); }
.uep-cine__arrow { display: none; }
.uep-cine__scroller.is-scrollable .uep-cine__arrow {
  display: inline-flex; align-items: center; justify-content: center;
  /* Centred on the THUMBNAILS, not on the strip. The strip is taller than the
     artwork because each chip carries its title underneath, so centring
     against the track left the arrows sitting low, level with the captions.
     Offset = half the thumbnail height minus half the button. */
  margin-top: calc((var(--uep-cine-chip) * 9 / 16 - 40px) / 2);
  width: 40px; height: 40px; flex: none;
  /* No disc. The chevron is already an unambiguous shape, and the ring was a
     second outline sitting beside a strip of hard-edged thumbnails. */
  border: 0; border-radius: 0; background: transparent;
  color: var(--uep-gold); cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
/* With the ring gone the fill has nothing to fill, so hover moves the chevron
   in the direction it will scroll instead. */
.uep-cine__arrow--prev:hover:not([disabled]) { transform: translateX(-3px); }
.uep-cine__arrow--next:hover:not([disabled]) { transform: translateX(3px); }
.uep-cine__arrow[disabled] { opacity: 0.3; cursor: default; }
.uep-cine__arrow:focus-visible { outline: 3px solid var(--uep-gold); outline-offset: 3px; }

/* --- "coming soon" / "view all" placeholder --------------------------------
   Two states, one composition. Below SETTLED the strip is short and says so;
   past CAP it is full and points onward. The markup is identical; only the
   words, the icon and the accent change. */
.uep-cine__soon { display: flex; }
/* The slot. Same box as a real thumbnail — 16:9, same radius, same navy — with
   a mark and a label in it, because an empty box beside real artwork reads as
   an image that failed to load rather than as a gap. */
.uep-cine__slot {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-3); width: 100%; aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  /* The icon inherits this; the label sets its own gold below. */
  color: var(--uep-on-dark);
  /* Nudged down: optically centred rather than mathematically centred, since
     the label's two lines sit heavier than the icon above them. */
  padding-top: var(--space-4);
  /* 2px transparent border to match .uep-cine__chip img, which carries one so
     the active state can turn it gold. Without it the placeholder sits 2px
     higher than the thumbnails beside it. */
  border: 2px solid transparent;
  box-sizing: border-box;
  background:
    repeating-linear-gradient(115deg, var(--uep-overlay-white-04) 0 12px, transparent 12px 24px),
    var(--uep-navy-deep);
}
.uep-cine__slotlabel {
  font-family: var(--font-headline); font-weight: 700; font-size: 0.66rem;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  line-height: 1.25; text-align: center; color: var(--uep-gold);
}
/* Icon and words are one target. The logo sits bare here, without the disc
   .uep-epirow__platform puts around it lower down the page: there the icon is
   one of three platform buttons in a row, so the pill groups them and gives
   each an equal hit area. Here it is a single mark next to its own label, and
   the ring only added a second shape around something that is already a button
   shape. The gap does the separating instead. */
.uep-cine__sublink {
  display: inline-flex; align-items: center; gap: var(--space-3);
  text-decoration: none; color: var(--uep-on-dark);
}
.uep-cine__platform {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  transition: transform var(--dur-fast) var(--ease-out);
}
.uep-cine__subtext {
  font-family: var(--font-headline); font-weight: var(--fw-sbd);
  font-size: 0.78rem; line-height: 1.2; color: var(--uep-on-dark);
}
.uep-cine__sublink:hover .uep-cine__subtext { color: var(--uep-gold); }
/* With the ring gone the icon needs its own hover cue, or only the words react. */
.uep-cine__sublink:hover .uep-cine__platform { transform: scale(1.06); }
.uep-cine__sublink:focus-visible { outline: 3px solid var(--uep-gold); outline-offset: 3px;
  border-radius: var(--radius-md); }

/* The View all tile. Same shape as the placeholder so it drops in unchanged,
   but green rather than gold: it is a live destination, not a note about
   something that does not exist yet. */
.uep-cine__soon--more .uep-cine__slot { color: var(--uep-green-light); }
.uep-cine__soon--more .uep-cine__slotlabel { color: var(--uep-green-light); }
.uep-cine__soon--more .uep-cine__sublink:hover .uep-cine__subtext { color: var(--uep-green-light); }

/* Chip footprint: 210px, slot above, link below, matching a real chip. The
   link is centred under the slot rather than left-aligned. In a row the
   artwork has a left edge for it to line up with; here the slot is the full
   width of the chip and its own contents are centred, so a left-hugging link
   under it reads as off-axis. */
.uep-cine__soon--chip {
  flex: 0 0 var(--uep-cine-chip, 210px); flex-direction: column; align-items: center;
  gap: var(--space-2); padding: 0 0 var(--space-3);
}
.uep-cine__soon--chip .uep-cine__slot { align-self: stretch; }
.uep-cine__soon--chip .uep-cine__sublink { align-self: center; }

/* The open player's close control. Its own backdrop, because a transparent
   button over video is unreadable half the time and invisible the other half. */
.uep-cine__player { position: relative; }
.uep-cine__close {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--radius-pill);
  background: var(--uep-overlay-ink-95); color: var(--uep-on-dark);
  font-size: 22px; line-height: 1; cursor: pointer;
}
.uep-cine__close:hover { background: var(--uep-gold); color: var(--uep-ink); }
.uep-cine__close:focus-visible { outline: 3px solid var(--uep-gold); outline-offset: 2px; }

/* .ffd-sec on /lab2/ puts its side padding on the SECTION and caps the inner
   wrap at --max-w-wide, so the content measure is a full 1200px. .uep-container
   caps its padding box instead, which lands the same content at 1136. Widen the
   cap by the padding so the shipping section measures identically to the lab. */
.uep-cine .uep-container { max-width: calc(var(--max-w-wide) + 2 * var(--space-5)); }
@media (min-width: 768px) {
  .uep-cine .uep-container { max-width: calc(var(--max-w-wide) + 2 * var(--space-6)); }
}

/* ==========================================================================
   ORIGIN  (.uep-origin)  —  founder story over a site photograph
   --------------------------------------------------------------------------
   Promoted from Origin E3 on /lab2/. A quote-led headline, the two proof
   numbers pulled out of the prose as a lockup, both founders' portraits, and
   a single capture card on white.

   The numbers are OUT of the paragraph deliberately. Set inline they read as
   adjectives and are skimmed past; set as a lockup at hero scale they are the
   second thing seen after the headline, which is the job they exist to do.

   The card is white on the photograph rather than cut out of it, so the ask
   reads as separate from the story. "Free Mini-Series" is green, not gold:
   gold on white measures 1.67:1, the same figure that got the gold focus ring
   pulled from this kit.
   ========================================================================== */
.uep-origin {
  position: relative; isolation: isolate;
  /* Only visible if the photograph fails to load — kept in the same family as
     the wash so a broken image degrades to a green section, not an ink one. */
  background: var(--uep-green-darker); color: var(--uep-on-dark);
  padding-block: var(--space-9);
}
.uep-origin__media { position: absolute; inset: 0; z-index: 0; }
.uep-origin__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Brand green rather than ink. Same three-stop shape as before: densest at the
   left where the headline and lede sit, opening up to the right so the site
   photograph still reads behind the white opt-in card.

   The left stop is --uep-green-darker (#046137), not --uep-green. At 0.95 over
   the photo it holds white text at roughly 7.4:1; flat --uep-green would land
   at 3.87:1, which is the figure that got gold text pulled from this kit. The
   two open stops ARE --uep-green (#069550) — nothing but the white card sits
   on them, so the brand colour is what a visitor actually sees across most of
   the band. */
.uep-origin__wash {
  position: absolute; inset: 0;
  background: linear-gradient(to right, rgba(4,97,55,0.95) 0%, rgba(6,149,80,0.78) 62%,
              rgba(6,149,80,0.52) 100%);
}
.uep-origin__inner { position: relative; z-index: 1; display: grid; gap: var(--space-7); }
@media (min-width: 1024px) {
  .uep-origin__inner { grid-template-columns: 1.1fr 0.9fr; gap: var(--space-8); align-items: center; }
}
.uep-origin__h {
  font-family: var(--font-headline); font-weight: 800;
  font-size: clamp(1.9rem, 4vw, 3rem); line-height: 1.04; letter-spacing: -0.02em;
  color: var(--uep-on-dark); max-width: 16ch; margin: 0 0 var(--space-4);
}
.uep-origin__lede {
  font-size: var(--fs-lead); line-height: var(--lh-body);
  /* White, not --uep-on-dark-muted. This lede carries the two marked facts and
     the company name, so it is doing headline work rather than sitting back as
     supporting copy. On the green wash below it that is 7.31:1, measured
     against the composited ground rather than assumed. <strong> inside it
     inherits, which is what makes "Salt Development Projects" white too. */
  color: var(--uep-on-dark); max-width: 58ch; margin: 0 0 var(--space-4);
}
/* The two "before" facts, marked. A soft gold band rather than an underline:
   underlined non-link text inside a paragraph reads as a hyperlink, and these
   are the two phrases a reader is most likely to try to click. */
.uep-origin__mark {
  color: var(--uep-on-dark); font-weight: var(--fw-sbd);
  background: linear-gradient(to top, var(--uep-overlay-gold-28) 0 0.42em, transparent 0.42em);
  padding: 0 0.12em;
}
.uep-origin__stats { display: flex; gap: var(--space-8); margin: var(--space-6) 0 0; }
.uep-origin__stats > div { margin: 0; }
.uep-origin__num {
  font-family: var(--font-headline); font-weight: 800;
  font-size: clamp(2.75rem, 7vw, 5rem); line-height: 1; letter-spacing: -0.03em;
  /* Gold, not --uep-green: green on the washed photograph sits near 2:1. */
  color: var(--uep-gold); margin: 0;
}
.uep-origin__cap { font-size: var(--fs-small); color: var(--uep-on-dark-muted); margin: var(--space-2) 0 0; }
.uep-origin__faces { display: flex; align-items: center; gap: 0; margin-top: var(--space-7); }
.uep-origin__faces img {
  width: 104px; height: 104px; object-fit: cover; border-radius: var(--radius-pill);
  border: 4px solid rgba(255, 255, 255, 0.9); box-shadow: var(--shadow-md);
}
@media (min-width: 768px) { .uep-origin__faces img { width: 132px; height: 132px; } }
.uep-origin__faces img + img { margin-left: -26px; }
.uep-origin__names {
  margin: 0 0 0 var(--space-5);
  font-family: var(--font-headline); font-weight: var(--fw-sbd);
  font-size: var(--fs-small); line-height: 1.5; color: var(--uep-on-dark);
}
/* The bare span is the "·" separating the two names. Gold, so it reads as a
   brand mark rather than as punctuation somebody forgot to style. It is
   aria-hidden, so its contrast carries no reading weight — no screen reader
   announces it and nobody has to make out its shape to read the names.
   The company line is named so it is not caught by the same selector. */
.uep-origin__names span { color: var(--uep-gold); font-weight: var(--fw-reg); }
.uep-origin__names .uep-origin__co { color: var(--uep-on-dark); }

.uep-origin__card {
  background: var(--uep-surface); border-radius: var(--radius-lg);
  padding: var(--space-6); box-shadow: var(--shadow-lg); color: var(--uep-ink);
}
.uep-origin__kicker {
  font-family: var(--font-headline); font-weight: 700; font-size: var(--fs-overline);
  letter-spacing: var(--tracking-xwide); text-transform: uppercase;
  color: var(--uep-slate); margin: 0 0 var(--space-1);
}
.uep-origin__cardtitle {
  font-family: var(--font-headline); font-weight: var(--fw-bld);
  font-size: var(--fs-h3); line-height: var(--lh-snug); text-transform: uppercase;
  letter-spacing: -0.01em; color: var(--uep-green-darker); margin: 0 0 var(--space-2);
}
.uep-origin__sub { font-size: var(--fs-small); color: var(--uep-slate); margin: 0 0 var(--space-5); }
.uep-origin__field { display: grid; gap: 6px; margin-bottom: var(--space-4); }
.uep-origin__field label { font-size: var(--fs-small); font-weight: var(--fw-sbd); color: var(--uep-ink); }
.uep-origin__field input {
  width: 100%; padding: 12px 14px; border: 1px solid var(--uep-line);
  border-radius: var(--radius-md); background: var(--uep-surface);
}
.uep-origin__field input:focus-visible { outline: 3px solid var(--uep-green); outline-offset: 1px; }
/* A text link, never a second button. */
/* Centred under the full-width submit button it follows, so the card ends on
   a single axis rather than the button being centred and its alternative
   hanging off the left. */
.uep-origin__alt { font-size: var(--fs-small); color: var(--uep-slate);
  margin: var(--space-4) 0 0; text-align: center; }
/* nowrap so the link either sits on the same line as the question or moves
   to the next one INTACT. Without it the phrase broke as "…Book a Call with"
   / "Our Team" on a phone. */
.uep-origin__alt a { color: var(--uep-green-darker); font-weight: var(--fw-sbd);
  white-space: nowrap; }

/* ==========================================================================
   SCROLL SCRUB  (.uep-scrub)  —  a clip driven by scroll position
   --------------------------------------------------------------------------
   Promoted from the .fx scaffold on /lab/ and /lab3/, cut down to the one
   shape a shipping page needs: a full-bleed clip, no copy column, no chips.

   The TRACK is the scroll budget. Its height minus one viewport is the
   distance the clip takes to play through. Longer track, slower playback.
   The STAGE is what sticks; it is exactly one viewport tall.

   The clip is NOT a background. It carries meaning (this is what the work
   looks like), so it keeps an aria-label and a poster that stands alone.

   prefers-reduced-motion: app.js parks progress at 1 and never fetches the
   video, so the poster is the whole experience. Do not "fix" that by
   animating anyway.
   ========================================================================== */
.uep-scrub { position: relative; background: var(--uep-ink); }
.uep-scrub__track { position: relative; height: 280vh; }
.uep-scrub__stage {
  position: sticky; top: 0; height: 100svh; overflow: hidden;
  background: var(--uep-ink);
}
.uep-scrub__media { position: relative; width: 100%; height: 100%; overflow: hidden; }
.uep-scrub__media img,
.uep-scrub__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* The poster sits underneath and the video fades over it once it can paint.
   --loaded is written by app.js; without it a not-yet-seekable video shows a
   black rectangle where the poster should be. */
.uep-scrub__video { opacity: var(--uep-loaded, 0); transition: opacity var(--dur-med) var(--ease-out); }

/* A short track on a phone: 280vh of scroll for a 3.5s clip is a long thumb
   journey on a small screen, and the clip reads fine faster. */
@media (max-width: 767px) { .uep-scrub__track { height: 200vh; } }

/* ==========================================================================
   STORY  (.uep-story)  —  long-form video under its introduction
   --------------------------------------------------------------------------
   Promoted from "Our Story" on /lab2/. Copy first, then the player at the full
   width of the container. Stacked rather than split on purpose: at 1200 wide
   the frame is large enough to stop someone, where a 55% column beside text
   reads as an illustration of the paragraph rather than the thing itself.

   White ground. The section sits between the green coaches band and the ink
   episode collage, so a third dark block in that run would flatten all three.

   The player is a FACADE, not an iframe. This is a 1h40m video, and a live
   embed would pull roughly a megabyte of third-party script into the homepage
   and set a Google cookie before anyone chose to watch. The poster is served
   from our own origin for the same reason — an i.ytimg.com thumbnail would
   contact Google on load and defeat the point.

   It runs on .uep-video + initVideoPlay() in app.js, which already resolves an
   iframe's data-src on click and adds autoplay. No new script for this.
   ========================================================================== */
.uep-story {
  background: var(--uep-surface);
  color: var(--uep-ink);
  padding-block: var(--space-8);
}
/* Text keeps a reading measure; the player does not. */
.uep-story__intro { max-width: 62ch; margin-bottom: var(--space-7); }
.uep-story__h {
  font-family: var(--font-headline); font-weight: 800;
  font-size: var(--fs-h2); line-height: 1.04; letter-spacing: -0.02em;
  color: var(--uep-ink); margin: 0 0 var(--space-4);
}
.uep-story__lede {
  font-size: var(--fs-lead); line-height: var(--lh-body);
  color: var(--uep-slate); margin: 0 0 var(--space-4);
}
.uep-story__lede--quiet { font-size: var(--fs-body); }
.uep-story__lede:last-child { margin-bottom: 0; }
/* The two "before" facts, marked. Same gold band as .uep-origin__mark on the
   ink hero; on white the ink text carries it instead of white text. */
.uep-story__mark {
  color: var(--uep-ink); font-weight: var(--fw-sbd);
  background: linear-gradient(to top, var(--uep-overlay-gold-28) 0 0.42em, transparent 0.42em);
  padding: 0 0.12em;
}
/* The poster sits under the iframe. Both fill the 16:9 box .uep-video gives
   them. The iframe is held at visibility:hidden by .uep-video until it has a
   src (see the DEFECT FIX on the base component), so the poster is what shows
   before the click; setting src reveals the iframe over the top of it. */
.uep-story__poster {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.uep-story__meta {
  margin: var(--space-3) 0 0; font-size: var(--fs-small); color: var(--uep-slate);
}

/* ==========================================================================
   DEAL SLIDER — a pinned horizontal carousel for the 22 project slides.

   Promoted from the page-local .ocs-slider on /our-case-studies/ so
   /case-studies/ can use it from the kit rather than a second copy. Two
   deliberate changes were made on the way in:

     1. 100svh, not 100vh. The lab page used 100vh, which is the one unit this
        project bans — on iOS the address bar makes 100vh taller than the
        screen, so the pinned stage would sit slightly off and the last slide
        could never fully arrive.
     2. The slides carry srcset. All 22 full-size images are 8.3MB; the 640px
        variants already existed and were only being used by a layout that has
        since been removed.

   ONE NUMBER DRIVES EVERYTHING: --cp, how far the section has travelled from
   0 to 1. The track's X offset and the progress bar are both derived from it
   in CSS, so they cannot drift apart. --n-1 (slide count minus one) is set
   inline on the section because it depends on how many slides are rendered.
   ========================================================================== */
.uep-dealslider {
  position: relative;
  background: var(--uep-ink);
  /* The pin needs something to travel through: one screen for the stage
     itself, plus half a screen of scroll per slide after the first. */
  height: calc(100svh + var(--n-1) * 50svh);
}
.uep-dealslider__stage {
  position: sticky; top: 0; height: 100svh;
  display: flex; align-items: center; overflow: hidden;
}
.uep-dealslider__track {
  --item: min(74vw, 1040px);
  --gap: clamp(1rem, 2vw, 2rem);
  --stride: calc(var(--item) + var(--gap));
  display: flex; gap: var(--gap); list-style: none; margin: 0;
  /* First item starts centred; every later one is --stride further along. */
  padding: 0 calc((100vw - var(--item)) / 2);
  transform: translate3d(calc(-1 * var(--cp, 0) * var(--n-1) * var(--stride)), 0, 0);
  will-change: transform;
}
.uep-dealslider__item {
  flex: 0 0 var(--item);
  position: relative;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--uep-green);
  box-shadow: 0 2px 6px rgba(0,0,0,.4), 0 26px 60px rgba(0,0,0,.45);
  /* Neighbours sit back until they are the one being read. */
  opacity: .38; transform: scale(.9);
  transition: opacity var(--dur-med) var(--ease-out), transform var(--dur-med) var(--ease-out);
  content-visibility: auto;
  contain-intrinsic-size: auto 1040px auto 585px;
}
.uep-dealslider__item.is-current { opacity: 1; transform: scale(1); }
.uep-dealslider__item img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; }
.uep-dealslider__cap {
  position: absolute; inset: auto 0 0 0;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-6) var(--space-4) var(--space-3);
  background: linear-gradient(0deg, var(--uep-overlay-ink-95), transparent);
  font-family: var(--font-headline);
}
.uep-dealslider__cap b { font-size: var(--fs-small); font-weight: var(--fw-bld); color: var(--uep-on-dark); }
.uep-dealslider__cap i {
  font-style: normal; font-size: var(--fs-overline); font-weight: var(--fw-bld);
  letter-spacing: var(--tracking-wide); color: var(--uep-gold);
  font-variant-numeric: tabular-nums;
}
/* Taken out of the stage's normal flow on purpose. The track is a flex row
   ~26,000px wide, and while it is a sibling in the layout it stretches the
   stage to that width — which throws the controls thousands of pixels off to
   the right, out of the overflow-hidden window. Absolute, with an explicit
   100vw, keeps them where the viewport is. */
.uep-dealslider__ui {
  position: absolute; left: 0; bottom: 0; width: 100vw; z-index: 2;
  background: linear-gradient(0deg, var(--uep-ink) 40%, transparent);
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-5);
  padding: var(--space-5) var(--space-5) var(--space-7);
}
.uep-dealslider__btn {
  width: 46px; height: 46px; flex: 0 0 auto;
  border: 1px solid var(--uep-overlay-white-18);
  border-radius: 50%; background: transparent; color: var(--uep-on-dark);
  font-size: 18px; line-height: 1; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.uep-dealslider__btn:hover:not(:disabled) { background: var(--uep-green); border-color: var(--uep-green); }
.uep-dealslider__btn:disabled { opacity: .3; cursor: default; }
/* 22 dots would be confetti. A bar reads as position at a glance. */
.uep-dealslider__bar {
  flex: 1 1 auto; max-width: 420px; height: 3px;
  background: var(--uep-overlay-white-15);
  border-radius: var(--radius-pill); overflow: hidden;
}
.uep-dealslider__bar span {
  display: block; height: 100%; width: calc(var(--cp, 0) * 100%);
  background: var(--uep-gold);
}
.uep-dealslider__num {
  font-family: var(--font-headline); font-size: var(--fs-overline);
  font-weight: var(--fw-bld); letter-spacing: var(--tracking-xwide);
  color: var(--uep-on-dark); font-variant-numeric: tabular-nums; white-space: nowrap;
}
@media (max-width: 900px) {
  .uep-dealslider__track { --item: 86vw; }
  .uep-dealslider__item { opacity: .5; }
}
/* Pinning takes the scroll away from the reader, so reduced motion gets a
   plain horizontal snap scroller instead — same content, they drive it. */
@media (prefers-reduced-motion: reduce) {
  .uep-dealslider { height: auto; }
  .uep-dealslider__stage { position: static; height: auto; overflow: visible; }
  .uep-dealslider__track {
    transform: none; overflow-x: auto;
    scroll-snap-type: x mandatory; padding-inline: var(--space-5);
  }
  .uep-dealslider__item {
    opacity: 1; transform: none; scroll-snap-align: center;
    content-visibility: visible;
  }
  .uep-dealslider__ui { display: none; }
}

/* ==========================================================================
   VIDEO TESTIMONIAL RAILS (.uep-vidrail) — promoted from /lab6/ D3b.

   Two full-bleed rails on the brand green: seven portrait clips above, seven
   landscape below, each scrolling sideways and each running off the edge of
   the screen so there is visibly more than fits.

   NO TYPE ON THE GREEN. --uep-green is #069550 and white on it is 3.87:1 —
   fine for a heading, not for a caption. So the field carries no body type at
   all: each name sits on a white bar under its frame. The name itself is
   --uep-green (3.87:1 on white), which is the project's standing position on
   this colour rather than an oversight; --uep-green-darker is 7.58:1 and is
   the swap if it ever has to pass.

   THE DISC IS DECORATIVE. .uep-cine__play is reused from the kit as-is. It
   keeps pointer-events:none and aria-hidden; the <video controls> underneath
   is the real control, which is what the keyboard and a screen reader get.
   initVidRail() in app.js supplies the pointer affordance.
   ========================================================================== */
.uep-vidrail {
  background: var(--uep-green);
  color: var(--uep-on-dark);
  padding-block: var(--space-7);
  /* Full width out of whatever container it is dropped into. */
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  overflow: hidden;
}
.uep-vidrail__rail {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  scrollbar-color: var(--uep-overlay-white-25) transparent;
}
.uep-vidrail__rail:focus-visible { outline: 3px solid var(--uep-on-dark); outline-offset: -3px; }
.uep-vidrail__track {
  display: flex;
  gap: var(--space-5);
  margin: 0;
  padding: var(--space-6) 0;
  list-style: none;
}
.uep-vidrail__item { flex: 0 0 auto; scroll-snap-align: start; min-width: 0; display: flex; }
/* CENTRE THE GROUP ONCE IT STOPS OVERFLOWING.
   Seven tall tiles cap at 330px each, so from roughly 2,500px of viewport the
   rail has nothing left to scroll — and the tiles sat hard against the left
   with a growing band of bare green to their right, while the rail still
   announced itself as scrollable.

   `safe center` is the whole fix and it needs no breakpoint: it centres the
   group when the track fits, and falls back to `start` the moment the content
   overflows. Plain `center` would NOT be safe — an overflowing centred flex
   track pushes its first items past the scroll origin, where they cannot be
   reached by scrolling at all. Every width that currently scrolls is untouched. */
.uep-vidrail__track { justify-content: safe center; }
.uep-vidrail__card {
  flex: 1 1 auto; margin: 0;
  display: flex; flex-direction: column;
  background: transparent;
  /* Rounded to match the rest of the site's video surfaces — .uep-video is
     --radius-lg, .uep-dealslider__item and .uep-cine__frame are --radius-md.
     The radius goes on the CARD, not the media: the card is footage plus the
     white name bar under it, and rounding only the media would leave the bar's
     square top corners meeting the frame's rounded ones. overflow:hidden is
     what makes the clip apply to both children. */
  border-radius: var(--radius-md);
  overflow: hidden;
}

/* Portrait rail. WIDTH-DRIVEN, not height-driven: how many faces are in view
   is a horizontal question, and sizing from svh made the count across change
   with the window's HEIGHT. The height keeps an svh cap so a 9:16 tile cannot
   run away on a short window; past it, object-fit: cover absorbs the
   difference at object-position 50% 26%, which holds the face. */
.uep-vidrail__rail--tall { --uep-vidrail-w: clamp(200px, 20.5vw, 330px); }
.uep-vidrail__rail--tall .uep-vidrail__item { width: var(--uep-vidrail-w); }
.uep-vidrail__rail--tall .uep-vidrail__media { height: min(calc(var(--uep-vidrail-w) * 16 / 9), 62svh); }
/* Inset the first tile so the green shows down the left-hand side while the
   rail still runs off the right. scroll-padding MUST match the track padding:
   without it the rail snaps the first tile flush and scrolls the inset away. */
.uep-vidrail__rail--tall .uep-vidrail__track { padding-left: clamp(var(--space-4), 2.8vw, var(--space-7)); }
.uep-vidrail__rail--tall { scroll-padding-left: clamp(var(--space-4), 2.8vw, var(--space-7)); }

/* Landscape rail. Opens at its midpoint (initVidRail), so centre-aligned snap
   points — with start-aligned ones the snap drags it back to an item edge a
   long way off centre and fights the script. */
.uep-vidrail__rail--wide { --uep-vidrail-h: clamp(140px, 26svh, 280px); }
.uep-vidrail__rail--wide .uep-vidrail__item { width: calc(var(--uep-vidrail-h) * 16 / 9); scroll-snap-align: center; }
.uep-vidrail__rail--wide .uep-vidrail__media { height: var(--uep-vidrail-h); }
.uep-vidrail__rail--wide .uep-vidrail__track { padding-inline: clamp(var(--space-4), 2.8vw, var(--space-7)); }
.uep-vidrail__rail--wide .uep-cine__play { width: 48px; height: 48px; }
.uep-vidrail__rail--wide .uep-cine__play::after { border-width: 8px 0 8px 14px; }

.uep-vidrail__media {
  position: relative; overflow: hidden;
  background: var(--uep-navy-deep);
  /* Own stacking context: the disc sits over a video that transforms on hover,
     and without this the transform wins on some paint orders and the disc
     disappears behind the footage. */
  isolation: isolate;
  cursor: pointer;
}
.uep-vidrail__media video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; border: 0;
  background: var(--uep-navy-deep);
  transition: transform var(--dur-slow) var(--ease-out);
}
.uep-vidrail__media--land video { object-position: 50% 36%; }
.uep-vidrail__media--port video { object-position: 50% 26%; }
/* Chrome paints its own centred start button under ours. Hidden so there are
   never two discs on one frame. NOTE: this also removes Chrome's click-to-play
   on an unloaded clip, which is precisely why initVidRail() has to start the
   first one — see the note there before removing either half. */
.uep-vidrail__media video::-webkit-media-controls-start-playback-button {
  display: none;
  -webkit-appearance: none;
}
/* A <video controls> paints its panel permanently, so fourteen tiles at rest
   would be fourteen grey scrubbers across the artwork. It fades in on hover and
   on keyboard focus instead — and only where a pointer can hover: on touch
   (hover: none) it is left visible, or there would be no way to pause, seek or
   unmute once a clip had started. */
@media (hover: hover) {
  .uep-vidrail__media video::-webkit-media-controls-panel {
    opacity: 0;
    transition: opacity var(--dur-med) var(--ease-out);
  }
  .uep-vidrail__media:hover video::-webkit-media-controls-panel,
  .uep-vidrail__media:focus-within video::-webkit-media-controls-panel { opacity: 1; }
  .uep-vidrail__media:hover video { transform: scale(1.03); }
  /* The disc GROWS on hover rather than fading out. Fading it on the very
     gesture that presses it is what made it read as decoration. */
  .uep-vidrail__media:hover .uep-cine__play,
  .uep-vidrail__media:focus-within .uep-cine__play { transform: scale(1.08); }
}
.uep-vidrail__media .uep-cine__play {
  transition: opacity var(--dur-med) var(--ease-out), transform var(--dur-med) var(--ease-out);
}

/* The caption is the name and nothing else, so it holds no padding of its own. */
.uep-vidrail__cap { display: flex; flex-direction: column; gap: 0; min-width: 0; padding: 0; flex: 1 1 auto; }
.uep-vidrail__name {
  display: flex; align-items: center; flex: 1 1 auto;
  background: var(--uep-surface);
  color: var(--uep-green);
  font-family: var(--font-headline);
  font-weight: var(--fw-sbd);
  font-size: var(--fs-body);
  line-height: var(--lh-snug);
  padding: var(--space-2) var(--space-4);
  min-height: calc(2.5 * 1.25em);
  border-bottom: 2px solid var(--uep-gold);
}

/* Playback state. CSS cannot observe playback — there is no :playing selector
   — so the class is set by initVidRail() off the video's real play/pause/ended
   events, which means the disc also comes BACK when a clip is paused. */
.uep-vidrail__media.is-playing .uep-cine__play { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .uep-vidrail__media video,
  .uep-vidrail__media .uep-cine__play { transition: none; }
  .uep-vidrail__media:hover video,
  .uep-vidrail__media:hover .uep-cine__play { transform: none; }
}

/* Two words of a heading picked out in the brand green. --uep-green is 3.87:1
   on white and on --uep-surface-alt, which is under AA for body copy but this
   is heading-scale type at --fs-h3 and above, where 3:1 is the threshold — so
   unlike the small green type elsewhere on the site this one clears it. Colour
   only: no weight or size change, or the phrase stops reading as part of the
   sentence and starts reading as a label. */
.uep-title-mark { color: var(--uep-green); }

/* -------- Sticky book-a-call bar --------
   NEW COMPONENT, and the reason it is new rather than a page-local .join-
   rule: nothing in the kit did this job. .uep-header is sticky at the TOP of
   the viewport and carries the nav, and there was no persistent bottom-edge
   CTA anywhere in the tree. /join/ needs one because the calendar sits
   thousands of pixels below the fold and the in-flow buttons are a long
   scroll apart, but the pattern is not page-specific — any long-form page
   with one destination can mount it.

   IT IS CHROME, NOT A SECTION. Mount it AFTER </main> and before the footer
   partial. Being outside the content flow is what keeps it clear of the
   house rule of one button per section: the bar and every in-flow CTA point
   at the same anchor, so the page still has exactly one ask.

   NO-JS DEFAULT IS HIDDEN. The markup ships with the `hidden` attribute and
   initStickyBar() in app.js is what removes it. A visitor without JavaScript
   simply never sees the bar; the in-flow CTAs and the terminal calendar are
   untouched. Do not "fix" this by dropping `hidden` — an always-on fixed bar
   over the hero is the exact thing this component is designed not to be.

   IT MUST NEVER SIT OVER THE END OF THE PAGE. app.js re-hides it while the
   booking section or the footer is on screen, so it cannot obscure the
   calendar it points at, the footer links or the legal line. That is
   behaviour, not styling, but it is the constraint this component exists
   under — see initStickyBar().

   z-index 39 is deliberately BELOW the header's 40. On a short viewport where
   the two could meet, the nav wins.

   NOT A COUNTDOWN, NOT A SEAT COUNT, NOT A PRICE. The program is evergreen
   and priced on the call. __copy is a one-line reminder of what the page is
   about and nothing else. */
.uep-stickybar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 39;
  background: var(--uep-ink);
  border-top: 1px solid var(--uep-on-dark-faint);
  padding-block: var(--space-3);
  /* iPhone home-bar strip. env() falls back to 0px everywhere else, so this
     is one declaration rather than a query. */
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  transform: translateY(110%);
  transition: transform var(--dur-fast) var(--ease-out);
}
/* Belt to the UA's braces: nothing here sets `display`, so `hidden` already
   works — but a future layout declaration on the block would silently break
   the no-JS default, and this line makes that impossible. */
.uep-stickybar[hidden] { display: none; }
.uep-stickybar.is-on { transform: translateY(0); }

.uep-stickybar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

/* Under 640 the reminder line is DROPPED, not wrapped: at that width the
   button wants the whole row, and a sentence stacked above a CTA stops
   reading as chrome and starts reading as a banner. */
.uep-stickybar__copy {
  display: none;
  margin: 0;
  font-family: var(--font-body);
  font-weight: var(--fw-med);
  color: var(--uep-on-dark);
}
.uep-stickybar .uep-btn { flex: 1 1 auto; }
@media (min-width: 640px) {
  .uep-stickybar__copy { display: block; }
  .uep-stickybar .uep-btn { flex: 0 0 auto; }
}

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

/* ==========================================================================
   MOBILE TYPESETTING + LAYOUT CORRECTIONS
   Small, targeted fixes for things that only misbehave on a phone.
   ========================================================================== */

/* An opt-in line break that exists only below 768. Lets a heading be set as
   two balanced lines on a phone without forcing that break on desktop, where
   it fits on one. `display: none` on a <br> suppresses the break entirely. */
.uep-br-sm { display: none; }
@media (max-width: 767px) { .uep-br-sm { display: inline; } }

/* The two names may break BETWEEN people but never inside a person: the markup
   joins each first/last pair with &nbsp;, and this keeps the separator as the
   only legal break point. Without it a 371px screen set it as
   "Carson Bolt Shannon" / "Sharp". */
@media (max-width: 767px) {
  .uep-origin__names { text-wrap: balance; }
}

/* The marquee's 280px top inset is tuned for a desktop collage. On a phone it
   is most of a screen of empty ink above the wordmark before anything reads.
   ≥1024 already overrides this to 100px, so only the small tiers are changed.

   TWO SEPARATE LEVERS, and the obvious one is the wrong one. padding-top moves
   the COLLAGE: .uep-marquee__content is absolutely positioned, so it resolves
   against the padding box's top edge and does not shift with it (the note on
   the base rule says so). The wordmark and the heading are moved by `top` on
   the content, which is what actually brings the copy up the screen. */
@media (max-width: 767px) {
  /* The collage starts lower on a phone so the wordmark and heading have clear
     ink above them rather than tiles running up behind the type. */
  .uep-marquee { padding-top: 210px; }
  .uep-marquee__content { top: 64px; }
}

/* PORTRAIT RAIL AS A CENTRED CAROUSEL ON MOBILE.

   The desktop treatment insets the first tile on the left and runs off the
   right, which is a good desktop shape and the wrong phone shape: it puts a
   full tile hard against the left edge with dead space beside it and no sense
   that this is one of several.

   Centre-snapped instead, with the track's inline padding set to exactly half
   the leftover width so a tile lands in the middle of the screen and its
   neighbours show at the edges. The width is chosen to leave a real peek
   rather than a sliver: at 371px, 58vw is a 215px tile, 78px of padding each
   side and about a 54px shoulder of the next clip once the gap is taken off.

   scroll-padding-left is zeroed here. It exists on desktop to stop proximity
   snapping eating the left inset; with centre alignment it would drag the
   snap position off-centre by that same amount.

   NOTE: the FIRST tile has no left-hand neighbour to show — nothing exists
   before it. Both shoulders appear from the second card onward. Starting the
   rail scrolled to card two would show both immediately but would hide the
   first testimonial, so it opens on card one. */
@media (max-width: 767px) {
  .uep-vidrail__rail--tall {
    --uep-vidrail-w: 58vw;
    scroll-snap-type: x mandatory;
    scroll-padding-left: 0;
  }
  .uep-vidrail__rail--tall .uep-vidrail__track {
    padding-inline: calc((100vw - var(--uep-vidrail-w)) / 2);
  }
  .uep-vidrail__rail--tall .uep-vidrail__item { scroll-snap-align: center; }
}

/* Explore and Start Here are short link lists; one column each wastes most of
   the width and pushes Contact off the first screen of the footer. Two columns
   below 768. The brand block and the contact block stay full width — the blurb
   and the email both need the measure. */
@media (max-width: 767px) {
  .uep-footer__grid { grid-template-columns: 1fr 1fr; }
  .uep-footer__grid > .uep-footer__brand { grid-column: 1 / -1; }
  .uep-footer__grid > *:last-child { grid-column: 1 / -1; }
}

/* --- Cinema strip on mobile ------------------------------------------------
   Smaller chips and a full-bleed scroller, so more than one and a bit episodes
   read at once and the strip uses the whole screen rather than sitting inside
   the page gutter.

   FULL WIDTH COSTS THE ARROWS. .uep-cine__scroller.is-scrollable is a
   `auto 1fr auto` grid with a chevron in each outer column, so while they are
   rendered the strip can never reach the viewport edge. On touch the swipe is
   the control anyway — the arrows exist for mouse users, who are all above this
   breakpoint. Forcing one column back gives the strip the full width.

   scroll-padding-left MUST match the strip's padding-left. The chips snap on
   `x mandatory` at `start`, and without it the first snap scrolls the gutter
   away and the strip sits flush against the screen edge at rest — the same trap
   the portrait video rail hit. */
@media (max-width: 767px) {
  .uep-cine__scroller,
  .uep-cine__scroller.is-scrollable {
    --uep-cine-chip: 158px;
    grid-template-columns: 1fr;
    gap: 0;
    margin-inline: calc(var(--space-5) * -1);
  }
  .uep-cine__scroller.is-scrollable .uep-cine__arrow { display: none; }
  .uep-cine__strip {
    padding-inline: var(--space-5);
    scroll-padding-left: var(--space-5);
  }
}

/* --- Deal slider on mobile: a vertical list, not a carousel ----------------
   The pinned horizontal carousel is a desktop device. On a phone it costs
   ~11 screens of scroll to walk 22 slides, each slide arriving at 74vw on a
   371px screen — and the slides are dense financial tables, which is exactly
   the content you do not want to read through a letterbox.

   So below 768 the whole mechanism is dismantled: the track's 100svh-per-slide
   height collapses, the sticky stage becomes static, and the flex row becomes a
   flex column. Every deal is simply on the page, full width, in order.

   THE JS STOPS TOO. initDealSlider() returns early below this breakpoint, so
   nothing is measuring travel or writing --cp against a layout that no longer
   has a horizontal axis. The CSS here would survive it anyway (transform is
   overridden), but leaving a scroll handler running to compute a number nobody
   reads is waste, not safety.
   ========================================================================== */
@media (max-width: 767px) {
  .uep-dealslider { height: auto; }
  .uep-dealslider__stage {
    position: static;
    height: auto;
    display: block;
    overflow: visible;
  }
  .uep-dealslider__track {
    flex-direction: column;
    gap: var(--space-5);
    padding: var(--space-6) var(--space-5);
    transform: none;
    will-change: auto;
  }
  .uep-dealslider__item {
    width: auto;
    flex: 0 0 auto;
    /* Every slide is legible at rest — no neighbour dimming, nothing scaled
       back, because there is no "current" one in a list. */
    opacity: 1;
    transform: none;
    scroll-snap-align: none;
    content-visibility: visible;
  }
  /* Prev/next and the progress bar describe a horizontal position that no
     longer exists. */
  .uep-dealslider__ui { display: none; }
  /* The caption is an absolutely-positioned bar carrying a dark gradient, the
     project name and its number, laid over the bottom of each slide. On a phone
     it is pure duplication: every slide already has its title set inside the
     artwork, so the overlay repeats the same words over the top of them and
     darkens the financials underneath while doing it.
     Nothing is lost to assistive tech — each slide's alt text already names the
     project ("<name> — project summary with costs, duration and net profit"),
     so the name survives with or without this bar. */
  .uep-dealslider__cap { display: none; }
}

/* ---------- Episode player modal ----------
   Opened by initEpisodePlayer() for any .uep-epirow row that carries a video
   id. Episode artwork sits in a narrow list column, so playing in place would
   put an hour-long interview in a thumbnail; this gives it the width it needs
   and leaves the list untouched behind it.

   Native <dialog>, same as .uep-authmodal: showModal() supplies the focus trap,
   the Escape handler, ::backdrop and inert-ing of the page, so none of that is
   reimplemented here. Wider than the auth modal because the payload is a 16:9
   player rather than a form. */
.uep-vidmodal {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--uep-on-dark);
  width: 100%;
  max-width: min(1100px, calc(100vw - 2 * var(--space-5)));
  overflow: visible;
}
.uep-vidmodal::backdrop {
  background: rgba(12, 19, 32, 0.82);
  backdrop-filter: blur(3px);
}
.uep-vidmodal__panel { position: relative; }

/* Same 16:9 contract as .uep-video, and it carries data-uep-video so the
   tracking hook documented in backend/tracking/meta-pixel.md finds it here
   exactly as it does an inline player. */
.uep-vidmodal__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--uep-ink);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
}
.uep-vidmodal__frame iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
}
.uep-vidmodal__title {
  margin: var(--space-4) 0 0;
  font-size: var(--fs-small);
  color: var(--uep-on-dark-muted);
  text-align: left;
}

/* Sits clear of the frame on desktop so it never covers the player's own
   controls; tucks inside on narrow screens where there is no room beside it. */
.uep-vidmodal__close {
  position: absolute;
  top: calc(var(--space-6) * -1);
  right: 0;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  font-size: 28px; line-height: 1;
  color: var(--uep-on-dark);
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.uep-vidmodal__close:hover { background: var(--uep-overlay-white-08); }
.uep-vidmodal__close:focus-visible { outline: 2px solid var(--uep-green); outline-offset: 2px; }

@media (max-width: 640px) {
  .uep-vidmodal__close {
    top: var(--space-3);
    right: var(--space-3);
    background: rgba(12, 19, 32, 0.6);
  }
}

/* ---------- Mini-series opt-in dialog ----------
   Same native <dialog> contract as .uep-authmodal: showModal() supplies the
   focus trap, Escape, ::backdrop and inert-ing of the page. Wider, because the
   payload is a five-field vendor form rather than a single input. */
.uep-optinmodal {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--uep-ink);
  width: 100%;
  max-width: min(560px, calc(100vw - 2 * var(--space-5)));
  overflow: visible;
}
.uep-optinmodal::backdrop {
  background: rgba(12, 19, 32, 0.72);
  backdrop-filter: blur(2px);
}
/* THE PANEL OWNS THE SCROLL, NOT THE IFRAME. form_embed.js writes an inline
   height on the frame and re-parents it into a container of its own, so a
   height set here would be both overridden and orphaned. Cap the panel and let
   it scroll; anything else puts the submit button out of reach on a short
   screen. */
.uep-optinmodal__panel {
  position: relative;
  background: var(--uep-surface);
  --uep-ring: var(--uep-ink);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
  padding: var(--space-6);
  max-height: calc(100vh - 2 * var(--space-5));
  overflow-y: auto;
  text-align: left;
}
/* THE FLOOR IS ON THE MOUNT, NOT THE FRAME. Reserving the space here keeps the
   dialog from collapsing before the widget reports its size, while leaving the
   frame's own height honest — initOptinModal reads it to decide whether the
   vendor script ever ran, and a floor on the frame made that read meaningless.
   Never a height on the frame: form_embed.js writes an inline one and moves the
   frame out of this container, so anything set here is both overridden and
   orphaned. */
.uep-optinmodal__embed {
  min-height: 520px;
}
.uep-optinmodal__embed > iframe {
  width: 100%;
  border: 0;
}
/* Sits under the card's own form, so it matches its siblings rather than
   inheriting body defaults beside them. */
.uep-origin__note {
  margin: var(--space-4) 0 0;
  font-size: var(--fs-small);
  color: var(--uep-slate);
}
.uep-optinmodal__title {
  font-family: var(--font-headline);
  font-weight: var(--fw-bld);
  font-size: var(--fs-h4);
  line-height: var(--lh-snug);
  margin: 0 0 var(--space-3);
  padding-right: var(--space-6);
}
.uep-optinmodal__lede {
  font-size: var(--fs-small);
  color: var(--uep-slate);
  margin: 0 0 var(--space-5);
}
.uep-optinmodal__legal {
  margin: var(--space-4) 0 0;
  font-size: var(--fs-small);
  color: var(--uep-slate);
}
.uep-optinmodal__legal a {
  color: var(--uep-green-darker);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.uep-optinmodal__close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 40px; height: 40px;
  display: grid; place-items: center;
  font-size: 28px; line-height: 1;
  color: var(--uep-slate);
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.uep-optinmodal__close:hover { background: var(--uep-overlay-ink-10); color: var(--uep-ink); }
.uep-optinmodal__close:focus-visible { outline: 2px solid var(--uep-green); outline-offset: 2px; }
