/* PJL Web 2.0 — Wireframe base styles
   Wireframe-fidelity only: structure and hierarchy, not final visual design. */

/* Brand fonts (assets/fonts/, sourced from ../pjl-brand/), ported from
   Web 1.1: PJL Display Bold Italic for top-level nav labels and page-
   title bars, PJL Sans (Bold and Regular, both registered under one
   family so font-weight picks the right cut) for dropdown-menu text.
   Everything else on the site keeps the system-font stack (--font-sans).
   PJL Display Bold (upright, non-italic) added alongside the Bold
   Italic cut above — both registered under the same "PJL Display"
   family, one per font-style, so font-style:normal/italic picks the
   right cut automatically wherever this family is used (e.g.
   .page-title wants Bold, not italic — the family's italic-only
   cut used everywhere else on the site can't be de-italicized via CSS,
   font-style on an already-italic outline doesn't synthesize upright,
   so this second face was pulled in from ../pjl-brand/fonts/ instead). */
@font-face {
  font-family: "PJL Display";
  src: url("assets/fonts/PJLDisplay-BoldItalic.otf") format("opentype");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "PJL Display";
  src: url("assets/fonts/PJLDisplay-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "PJL Sans";
  src: url("assets/fonts/PJLSans-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "PJL Sans";
  src: url("assets/fonts/PJLSans-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* PJL brand palette — every ramp step actually used on the site, named
   exactly as ../pjl-brand/pjl-color-palette.md does (e.g. --navy-500).
   Every token below this block is *semantic* (--brand-navy, --surface-0,
   --complete-bg, etc.) and should point here rather than hardcode a hex
   value — this table is the only place a palette hex needs to live.
   Only the steps some rule actually references are listed here; when a
   new step gets used, add it here (copying its hex from the palette doc)
   rather than pre-populating the whole 13-step ramp. */
:root {
  /* Navy — color.brand.primary */
  --navy-25: #EEF6FF;
  --navy-50: #B6D1ED;
  --navy-200: #6C859F;
  --navy-300: #4A627B;
  --navy-500: #0C2237; /* base */
  --navy-700: #02162A;
  --navy-800: #001124;

  /* Orange — color.brand.secondary */
  --orange-25: #FFF2EE;
  --orange-500: #FF4800; /* base */

  /* Brown — color.brand.tertiary */
  --brown-500: #522311; /* base */

  /* Sand — color.brand.accent-1 */
  --sand-25: #FFF5D8;
  --sand-500: #CABD98; /* base */

  /* White — color.brand.accent-2 (cool-tinted off-white ramp, distinct
     from Absolute White/--ab-white below) */
  --white-50: #F3F3F9;
  --white-500: #E2E2E8; /* base */
  --white-600: #B6B6BC;
  --white-700: #8C8C92;
  --white-950: #1E1E22;

  /* Absolute White — color.ab-white */
  --ab-white: #FFFFFF;

  /* Grey — color.neutral.grey */
  --grey-50: #F8F9FA;
  --grey-300: #DEE2E6;
  --grey-700: #495057;

  /* Emerald — color.emerald (Success) */
  --emerald-25: #DFFFE4;
  --emerald-500: #15803D; /* base */
  --emerald-700: #005222;

  /* Amber — color.amber (Warning) */
  --amber-25: #FFF2EC;
  --amber-500: #B45309; /* base */
  --amber-700: #753200;

  /* Cobalt — color.cobalt (Info) */
  --cobalt-25: #F0F5FF;
  --cobalt-500: #155EEF; /* base */
  --cobalt-700: #0037A5;
}

:root {
  --surface-0: var(--grey-50);
  /* Same primitive as --ab-white above, kept as its own token — this
     one's the page's general background surface, --ab-white is for
     places (button rollover text, etc.) that want "white" as a color
     choice rather than "the surface" specifically. */
  --surface-1: var(--ab-white);
  --border: var(--grey-300);
  --border-strong: var(--white-600);
  --text-primary: var(--white-950);
  --text-secondary: var(--grey-700);
  --text-muted: var(--white-700);
  /* Event-state event cards — each state its own brand hue (Complete:
     Sand, Next/Current: Orange, Upcoming: Navy) rather than variations
     of white/navy, so the three are distinguishable by hue alone, not
     just tint depth. -25 tint background, -500 solid border for each
     state's own hue; text stays --text-primary across all three states
     (only fill/border vary), rather than each state's own -700 shade.
     Orange is otherwise used only for page-title-bar at this time, so
     Next/Current claiming it doesn't compete with any other sitewide
     usage. Complete/Upcoming swapped from their original Navy/Sand
     pairing — trying Sand for Complete (past, settled) and Navy for
     Upcoming (further out, more weight) instead. */
  --complete-bg: var(--sand-25);
  --complete-border: var(--sand-500);
  --complete-text: var(--text-primary);

  --emphasized-bg: var(--orange-25);
  --emphasized-border: var(--orange-500);
  --emphasized-text: var(--text-primary);

  /* Tickets action button — Emerald, unclaimed by any other button/CTA
     on the site (only used elsewhere as the News "Commerce" category
     tag color). Distinct from Watch Live's orange and Results' navy. */
  --tickets-btn-border: var(--emerald-500);
  --tickets-btn-text: var(--ab-white); /* light text once filled on hover — see .btn-emerald:hover */

  /* Badge base — Cobalt */
  --badge-bg: var(--cobalt-25);
  --badge-border: var(--cobalt-500);
  --badge-text: var(--cobalt-700);
  --radius: 0;
  --font-sans: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;

  /* Sitewide nav brand colors — Navy ramp, in two treatments: "solid"
     (dark filled bar) and "outline" (light tint + navy border). Main
     nav uses solid, eyebrow nav uses outline. Footer keeps its own
     fixed solid values (below) so it's unaffected if this mapping changes. */
  --nav-solid-bg: var(--navy-500);
  --nav-solid-text: var(--navy-25);
  --nav-solid-text-muted: var(--navy-50);
  --nav-solid-divider: var(--navy-300);
  --nav-solid-hover: var(--navy-50);

  --nav-outline-bg: var(--navy-25);
  --nav-outline-border: var(--navy-500);
  --nav-outline-text: var(--navy-700);
  --nav-outline-text-strong: var(--navy-800);
  --nav-outline-divider: var(--navy-200);
  /* A flat navy-25/-50 swap read as too strong here, and some cards this
     hovers (e.g. the Rider/Team Directory's own .cat-editorial rows)
     already rest at navy-25 — a flat navy-25 hover would be invisible
     on those. A low-alpha navy wash instead stays subtle everywhere and
     still shifts visibly against any resting fill, white or tinted. */
  --nav-outline-hover-bg: rgba(12, 34, 55, 0.04); /* navy-500 @ 4% */
  --nav-outline-accent: var(--navy-500);

  /* Footer — fixed solid-navy values, independent of the nav mapping above */
  --footer-bg: var(--navy-500);
  --footer-text: var(--navy-25);
  --footer-divider: var(--navy-300);
  --footer-hover: var(--navy-50);

  /* Brand primaries — from ../pjl-brand/brand.md, used by .page-title-bar */
  --brand-orange: var(--orange-500);
  --brand-navy: var(--navy-500);

  /* Brand accents — used by .leg-divider */
  --brand-sand: var(--sand-500);   /* Brand Accent 1 */
  --brand-white: var(--white-500); /* Brand Accent 2 */
  --brand-brown: var(--brown-500); /* Brand Tertiary — #3/Top Rider badges */

  --upcoming-bg: var(--navy-25);
  --upcoming-border: var(--navy-500);
  --upcoming-text: var(--text-primary);

  /* Round-complete card (Winning Team / Top Rider blocks) — deliberately
     hue-neutral rather than pulled from any one state's family. It
     nests inside all three event states (Complete/navy, Next-Current/
     orange, Upcoming/sand) and also sits directly on the plain page
     background (Event Detail's Daily Schedule/Results tab), so a
     family-specific fill (sand-25, tried earlier) inevitably matches
     one state's own tint and nearly disappears there — Upcoming's own
     fill is that same sand-25. White reads as a distinct card against
     every -25 tint and the page's off-white equally, the same way
     .module's white-on-off-white already works elsewhere; the navy
     border/text carry the contrast instead of the fill color. */
  --round-complete-bg: var(--surface-1);
  --round-complete-border: var(--brand-navy);
}

* { box-sizing: border-box; }

/* Disables mobile browsers' own automatic text-inflation heuristic
   (WebKit/Blink boost small text differently depending on the
   surrounding column's width/content density, entirely independent of
   any CSS font-size) — without this, two elements with byte-for-byte
   identical computed font-size can still render at visibly different
   sizes on a phone, which is exactly what made the Winner/Runner-up/
   Top Rider mini-row badges (styles.css: .mini-row-badge) look
   mismatched in width-constrained text despite sharing one font-size
   rule; verified in this project's own desktop browser tooling that
   the CSS itself was already identical, which only left a mobile-only
   rendering heuristic as the explanation. 100% opts every element out
   of that adjustment rather than letting the browser decide per
   element. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* Sitewide focus-outline convention — brand navy by default (recolored
   from the browser's default blue), flipped to brand orange only for
   controls sitting directly on their own navy-fill container (.main-nav,
   .footer-nav, .bottom-nav, .section-label-bar), where a navy outline
   would disappear against the navy behind it. A native `outline` (not
   box-shadow) so it draws just outside the element regardless of the
   element's own fill — the deciding factor is the CONTAINER's
   background, not the focused element's own (e.g. .filter-button fills
   navy on hover, but its container is light, so navy focus still reads
   fine there; no override needed).
   :focus-visible, not plain :focus — this should only appear for
   keyboard tabbing, not every mouse click (a plain :focus rule shows on
   both, which isn't how focus rings worked here before this convention
   was added). :focus-visible is exactly the "pressed" vs "focused"
   distinction: a mouse click still focuses the element, but the browser
   only flags it :focus-visible when the input modality calls for a
   visible indicator (keyboard use, mainly). */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--brand-navy);
  outline-offset: 1px;
}
.main-nav a:focus-visible,
.main-nav button:focus-visible,
.footer-nav a:focus-visible,
.bottom-nav a:focus-visible,
.section-label-bar a:focus-visible,
.section-label-bar button:focus-visible {
  outline-color: var(--brand-orange);
}
/* Dropdown popups are always a light surface regardless of their
   trigger nav (see .main-nav .dropdown-menu's own comment) — so their
   items stay navy even though they're nested inside .main-nav's own
   navy fill, overriding the orange rule just above. Higher specificity
   (two classes, not one) wins outright, no !important needed. */
.main-nav .dropdown-menu-item:focus-visible,
.eyebrow-nav .dropdown-menu-item:focus-visible {
  outline-color: var(--brand-navy);
}

/* Sticky-footer layout, ported from Web 1.1: body is a full-viewport-
   height flex column, and .page (flex:1, below) grows to absorb any
   leftover space, pushing bottom-nav/footer down to the true bottom of
   the view on short-content pages. On tall pages .page's own content
   already exceeds 100vh, so flex:1 has no leftover space left to fill
   and this has no effect — bottom-nav/footer scroll below it normally,
   same as before. */
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* Several full-bleed fills (.page-title-bar, .tab-nav's underline)
     use width:100vw to break out of .page's own centered max-width;
     100vw includes the vertical scrollbar's own width in most browsers,
     which can overshoot the true available width by a few px and
     trigger an unwanted horizontal scrollbar. Guards against that
     sitewide rather than per-rule. */
  overflow-x: hidden;
  background: var(--surface-0);
  color: var(--text-primary);
  font-family: var(--font-sans);
}

/* Single source of truth for the site's outer content column (.page's
   own max-width) and the content bounds inside it — i.e. where .page's
   modules actually render, after .page's own 32px left/right padding
   is subtracted (16px top/bottom, unchanged — only the horizontal
   value feeds this column-width math). Full-bleed bars (eyebrow/main
   nav, bottom nav, footer — see each below) size their own inner
   wrapper to --content-bounds, then apply --content-edge (below) on
   top of it, so their content lands at exactly the same left/right
   edges as .page's own modules, while the bar's fill/border stretches
   to the true viewport edge. Fluid, not breakpoint-gated: fills the
   viewport from 1080px up to a 1600px ceiling, with no jump at any
   specific breakpoint. */
:root {
  --page-max-width: clamp(1080px, 100vw, 1600px);
  --content-bounds: calc(var(--page-max-width) - 64px);
  /* Where a module's own OUTER edge (its border) sits — full-bleed
     bars/content whose own content should align with a module's
     bounding BOX (not its inner text) use this directly as their own
     inset. Every .module sits flush against .page's own content edge
     with no extra margin of its own, so this is that same edge. The
     32px floor matches .page's own left/right padding above — without
     it, this would silently collapse to 0 below the 1080px viewport
     floor (where --content-bounds stops shrinking with the real
     viewport, so the (100vw - content-bounds)/2 half no longer tracks
     anything meaningful on its own). */
  --content-edge: max(32px, calc((100vw - var(--content-bounds)) / 2));
  /* A module's own INNER text position instead — --content-edge plus
     the module's own +16px padding (module padding is unchanged by
     the page-margin increase above — a separate, internal-to-the-card
     convention). .page-title-bar's own left inset uses this (see its
     own comment); factored out here (not just inlined there) so
     .page-title-bar-back below can negate it (bleeding its own fill
     out to the bar's true edge) and reapply it as its own padding,
     without duplicating this whole expression. */
  --title-bar-inset: calc(var(--content-edge) + 16px);
}

.page {
  max-width: var(--page-max-width);
  margin: 0 auto;
  /* Left/right 32px, top/bottom unchanged at 16px — see --content-bounds/
     --content-edge below, which this value feeds directly; every other
     bar's own content-alignment derives from these two, so changing
     this is enough to move the whole site's left/right content edge in
     lockstep with no further edits needed at each individual module.
     Reduced to 16px below the md/tablet breakpoint (768px) — see the
     matching --content-edge override below, which is what keeps every
     full-bleed bar's own content (nav, footer, breaking bar, etc.)
     landing at this same edge instead of the two silently drifting
     apart below that width. */
  padding: 16px 32px;
  /* Grows to fill leftover space in body's flex column — see body's own
     sticky-footer comment above. */
  flex: 1 0 auto;
  width: 100%;
}
/* Tablet-and-below page margin — 16px instead of the desktop 32px.
   Scoped to md's own 768px threshold (see the sm/md/lg/xlg scale
   documented further down this file), not the narrower 640px "mobile"
   breakpoint used elsewhere, since this is specifically a page-margin
   reduction, not a layout-shape change (stacking columns, hiding
   content, etc.) — those still make their own separate calls at
   whatever breakpoint suits them.
   --content-edge only needs a flat 16px here, not
   max(16px, calc(...)): below the 1080px --page-max-width floor,
   --content-bounds is already pinned at a fixed 1016px regardless of
   the real viewport, so that calc() term is always deeply negative in
   this range anyway — a flat value says the same thing without
   restating dead logic. Every full-bleed bar's own content padding
   (--content-edge) and --title-bar-inset (--content-edge + 16px, see
   its own definition) update in lockstep automatically, since they're
   both plain var()/calc() reads, not separately duplicated values. */
@media (max-width: 767.98px) {
  :root {
    --content-edge: 16px;
  }
  .page {
    padding-left: 16px;
    padding-right: 16px;
  }
}

/* Typography */
.th { font-size: 14px; font-weight: 500; margin: 0 0 4px; }
.ts { font-size: 14px; color: var(--text-secondary); margin: 0; }
.ts-muted { font-size: 14px; color: var(--text-muted); margin: 0; }

/* Standard content module */
.module {
  border: 0.5px solid var(--border-strong);
  border-radius: 0;
  padding: 12px;
  background: var(--surface-1);
  min-height: 70px;
  margin-bottom: 12px;
}

/* Event-state cards — a subtle indicator rather than a full-card tint:
   white fill, a neutral grey border, and a 4px solid left accent stripe
   in the state's own hue (still the same -500 tokens the full border
   used before). Text stays the state's -text token (navy-500 for all
   three, per the earlier revision). Each state's old -bg tint (-25) is
   repurposed below as its hover-only wash, rather than dropped. */
.module-emphasized {
  position: relative;
  border-radius: 0;
  padding: 12px;
  background: var(--surface-1);
  border: 0.5px solid var(--border-strong);
  border-left: 4px solid var(--emphasized-border);
  color: var(--emphasized-text);
  margin-bottom: 8px;
}

.module-complete {
  border-radius: 0;
  padding: 12px;
  background: var(--surface-1);
  border: 0.5px solid var(--border-strong);
  border-left: 4px solid var(--complete-border);
  color: var(--complete-text);
  margin-bottom: 8px;
}

/* Also styles "Next" cards (the specific event on deck once Current is
   underway — see .status-row's own comment for the full state cycle):
   Next is a label change only, not a separate visual treatment — it
   still renders as a plain .module-upcoming card until its own start
   date arrives and it picks up .module-emphasized's orange instead. */
.module-upcoming {
  border-radius: 0;
  padding: 12px;
  background: var(--surface-1);
  border: 0.5px solid var(--border-strong);
  border-left: 4px solid var(--upcoming-border);
  color: var(--upcoming-text);
  margin-bottom: 8px;
}

/* Round card state, in Daily Schedule/Results — a round with a result
   reads as "complete" (white/navy, hue-neutral so it nests cleanly
   inside any Event state — see --round-complete-bg above), a round
   with no result yet reads as "upcoming" (shares --upcoming-bg/-border
   with .module-upcoming, so the two stay in sync automatically) —
   independent of which Event page it's on. */
.round-complete {
  border-radius: 0;
  padding: 12px;
  background: var(--round-complete-bg);
  border: 0.5px solid var(--round-complete-border);
  color: var(--brand-navy);
  margin-bottom: 12px;
}
/* Winning Team / Top Rider label — always the block's first child <p> in
   its Results/Event-Detail usage (a bare .round-complete card, e.g.
   Standings' top-3, has no <p> there, just a .badge-corner span, so this
   doesn't touch those). */
.round-complete > p:first-child {
  text-transform: uppercase;
}
/* Winning Team / Top Rider's own image — bleeds to the card's own top,
   right, and bottom border (no inset from .round-complete's 16px
   padding), spanning the block's full height including the label line
   above .card-with-headshot, not just .card-with-headshot's own row.
   That full-height span is only reachable by taking the image out of
   flow entirely (position:absolute against .round-complete itself,
   whose padding EDGE is where an absolute child's 0-offsets land —
   i.e. flush with the border, ignoring the padding), since nesting
   (label <p>, then .card-with-headshot) means no in-flow ancestor
   wraps both while staying inside .card-with-headshot's own box.
   Scoped via :has(> p:first-child), the same "has the label paragraph"
   test used above, so this doesn't touch the bare .round-complete cards
   reused elsewhere (Standings/Rider Leaderboard top-3, Home's League
   Leaders) which have no label and should keep the sitewide
   image-left, inset layout. padding-right on the card reserves room so
   the in-flow label/text never runs under the bled image. */
/* Fixed (not content-driven) height — this block's height used to
   depend on how its Team/Rider Name and Country lines happened to wrap
   at whatever width a given breakpoint/column count left it, so the
   same block (and its 1:1 logo/headshot, sized off this height) came
   out a different size at different viewport widths. Truncating those
   lines to a single line each (below) means the content itself never
   grows past what this fixed height already assumes. 104px (was 90px)
   for a 4th text line — Top Rider's Team/Country now split onto their
   own lines instead of one combined "Team Name · Country" line, and
   Winning Team gets a blank spacer line in Team/Country's gap to match
   (see the markup), so both blocks read as the same 4-line shape. */
.round-complete:has(> p:first-child) {
  position: relative;
  padding-right: 132px;
  height: 104px;
  overflow: hidden;
}
/* min-width:0 so the flex child can actually shrink for its own
   nowrap+ellipsis text below to engage — a flex item's default
   min-width is auto (its content's width), which otherwise blocks
   text-overflow entirely. Truncated to one line rather than left to
   wrap: Name, Team Name, and Country are the lines with genuinely
   variable length (the label above and the time value below are both
   always short, and the blank spacer line has no content to truncate),
   so they're the only ones that could grow past this block's own fixed
   height. */
.round-complete:has(> p:first-child) .card-with-headshot > div {
  min-width: 0;
}
.round-complete:has(> p:first-child) .card-with-headshot > div > p:nth-child(1),
.round-complete:has(> p:first-child) .card-with-headshot > div > p:nth-child(2),
.round-complete:has(> p:first-child) .card-with-headshot > div > p:nth-child(3) {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Team logos and rider headshots must both land 1:1 — top:0 + bottom:0
   gives each a definite height matching the card exactly (reliable);
   width can't be derived from that height via CSS alone, though — tried
   `width:auto` + `aspect-ratio:1/1` first (the textbook way to do
   this), but browsers don't consistently run the
   aspect-ratio-from-a-definite-cross-size algorithm for an absolutely
   positioned box with top+bottom both set, so it silently fell back to
   this element's sitewide default width (40px) instead of the card's
   real height — a rectangle, not a filled square. include.js sets an
   explicit inline width instead, computed from this element's own
   rendered height at runtime, which sidesteps that gap entirely — for
   both of these, the same way. */
.round-complete:has(> p:first-child) .logo-placeholder,
.round-complete:has(> p:first-child) .headshot-placeholder {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  height: auto;
}
.round-upcoming {
  border-radius: 0;
  padding: 12px;
  background: var(--upcoming-bg);
  border: 0.5px solid var(--upcoming-border);
  color: var(--upcoming-text);
  margin-bottom: 12px;
}
/* Results' own "not yet run" placeholder (nested inside
   .event-rounds-grid, unlike Daily Schedule's plain round listing
   above, which uses the same .round-upcoming class unscoped) — fill
   and border now match .rider-tile/.team-tile instead of the amber
   "upcoming" treatment, so an unfinished round reads as a plain,
   neutral placeholder rather than a state warning. Text color is left
   unset here (rather than restated) so .ts-muted's own color takes
   over now that the child <p> no longer needs color:inherit to read
   against an amber fill. */
.event-rounds-grid .round-upcoming {
  background: var(--ab-white);
  border: 0.5px solid var(--border-strong);
}

/* Section label styled as a navy bar (e.g. "Venue") rather than plain
   text (e.g. "Teams") — used where a section should read as a distinct,
   bannered header instead of a quiet in-flow label. */
.section-label-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  background: var(--brand-navy);
  color: var(--brand-white);
  padding: 8px 14px;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  margin: 0 0 8px;
}
/* Event Detail's own Event ID Bar (Event ## + date) — the exact chain
   .section-label-bar -> .event-venue-bar -> .tab-nav only ever occurs
   on Event Detail (event-complete.html/event-next-current.html/
   event-upcoming.html); Results Landing/Full Results reuse the same
   .section-label-bar+.event-venue-bar pairing repeated once per Event
   card, never followed by a .tab-nav, so they're untouched by this.
   NOT full-bleed here (per feedback) — stays at the normal
   content-bounds width/inset every other usage of this bar already
   uses (Results Landing/Full Results), rather than the 100vw breakout
   .tab-nav/.page-title-bar use. Falls back to the base rule's own
   plain 14px padding once the --title-bar-inset/--content-edge
   overrides are gone, same as those other usages.
   Sticky again, now that .page-title's own sticky top/z-index are
   settled — top:155.6px (desktop)/151.2px (mobile; both +8px over
   their 2026-09-10 147.6/143.2, from .page-title's own padding-top
   16px→24px change) is .page-title's own stuck bottom (see its
   own comment, styles.css) rather than
   .pjl-rail-compact's, so this bar picks up immediately where .page-title
   ends instead of overlapping its still-visible text (the bug that
   forced this bar non-sticky for a while — see .event-venue-bar's own
   comment just below for what that looked like and why). Same z-index
   (28) as every other bar occupying this "sub-nav row" layer sitewide
   (.tab-nav/.filter-row/.status-row/.page-title) — not decremented per
   additional stacked layer; source order alone resolves paint order
   correctly for sticky siblings scrolling past each other in sequence,
   same as this chain's own original (pre-.page-title-rollout)
   implementation did. */
.section-label-bar:has(+ .event-venue-bar + .tab-nav) {
  position: sticky;
  top: 155.6px;
  z-index: 28;
}
@media (max-width: 639.98px) {
  .section-label-bar:has(+ .event-venue-bar + .tab-nav) {
    top: 151.2px;
  }
}
/* Sitewide link styles — two variants, both bold/no-underline, chosen
   by what the link sits on. .link-orange is the default (any plain
   page/module/card surface); .link-sand is for a link sitting directly
   on a navy fill (.section-label-bar — orange-on-navy has much lower
   contrast than sand-on-navy). .back-link (below) stays its own
   separate, un-bolded style. Applied alongside each element's own
   layout class (.section-label-bar-action, .show-more-toggle) or
   directly on a plain <a> (article-footer). */
.link-orange {
  color: var(--brand-orange);
  font-weight: 700;
  text-decoration: none;
}
.link-orange:hover {
  color: var(--brand-navy);
}
.link-sand {
  color: var(--sand-500);
  font-weight: 700;
  text-decoration: none;
}
.link-sand:hover {
  color: var(--white-500);
}
.section-label-bar-action {
  font-size: 12px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  white-space: nowrap;
}

/* Event ID Bar — the reusable pairing of a .section-label-bar (Event
   Name + date, via .bar-divider) with this .event-venue-bar (Venue +
   Location, via .bar-divider) directly beneath it, identifying which
   Event a page's content belongs to. Reused as-is wherever Event
   content is designated — Results Landing (results.html, once per
   Event card), Full Results (event-results-complete.html/
   event-results-next-current.html), and Event Detail
   (event-complete.html/event-next-current.html/event-upcoming.html) —
   each time with .page-title-bar itself reduced to a generic "Event
   Detail"/"Event Results" label rather than the Event's own identity,
   which now lives here instead. The section-label-bar above this one
   has its own bottom margin zeroed locally (style="margin-bottom:0")
   so the two sit flush with no gap between them. Padding is one px
   shorter on the bottom than .section-label-bar's to offset this bar's
   own border-bottom, so the two bars render the same total height.
   No special-cased margin of its own beyond the site's standard 12px
   block spacing (matches .module/.grid-2) — a page needing more room
   before whatever follows this bar adds it locally there rather than
   this shared component carrying an oversized default for everyone. */
.event-venue-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  background: var(--surface-0);
  color: var(--brand-navy);
  border-bottom: 1px solid var(--brand-navy);
  padding: 8px 14px 7px;
  font-size: 14px;
  font-weight: 500;
  text-transform: uppercase;
  margin: 0 0 12px;
}
/* Event Detail's own tab-nav sits directly below this bar (no other
   content between them) — same gap-absorption as .page-title +
   .tab-nav above (this bar's own margin-bottom moves to being
   .tab-nav's padding-top instead), so the space above the tab row
   matches every other .tab-nav on the site (e.g. Standings) instead of
   this bar's own standard 12px margin, which is meant for the Results
   Landing/Full Results usages that don't lead straight into a
   .tab-nav.
   NOT full-bleed here (per feedback) — same as .section-label-bar
   just above (its own comment): stays at the normal content-bounds
   width/inset every other usage of this bar already uses (Results
   Landing/Full Results), falling back to the base rule's own plain
   14px padding.
   Sticky again, now that .page-title's own sticky top/z-index are
   settled — same reasoning as .section-label-bar just above (its own
   comment). top:188.6px (desktop)/184.2px (mobile; both +8px over
   their 2026-09-10 180.6/176.2, from .page-title's own padding-top
   16px→24px change) is .section-label-bar's own stuck bottom
   (164.3/146.7 + its 33px height), so this bar picks up immediately
   where THAT bar ends.
   .tab-nav right after it keeps its own sticky too — it now sticks
   below BOTH of these (top:220.6px/216.2px, .event-venue-bar's own
   stuck bottom = 201.8/184.2 + this bar's 32px height — see that
   rule's own override just below), not directly under .page-title
   anymore now that this bar and .section-label-bar are back in the
   stack ahead of it. */
.event-venue-bar:has(+ .tab-nav) {
  margin-bottom: 0;
  position: sticky;
  top: 188.6px;
  z-index: 28;
}
@media (max-width: 639.98px) {
  .event-venue-bar:has(+ .tab-nav) {
    top: 184.2px;
  }
}
.event-venue-bar + .tab-nav {
  padding-top: 16px;
  top: 220.6px;
}
@media (max-width: 639.98px) {
  .event-venue-bar + .tab-nav {
    top: 216.2px;
  }
}

/* Status badge (e.g. Live — a temporary sub-state layered on a label).
   Base color is Cobalt; a state variant (e.g. .badge-live) can override
   background/border/color here if it ever needs a distinct color. */
.badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: 0;
  margin-left: 8px;
  vertical-align: middle;
  background: var(--badge-bg);
  border: 0.5px solid var(--badge-border);
  color: var(--badge-text);
}
/* A badge leading its own card/line (e.g. a News card's category tag)
   has nothing to its left to separate from — margin-left: 8px there
   would only misalign it from the card's own left edge. */
.badge:first-child {
  margin-left: 0;
}
/* Explicit version of the same fix for a badge that leads its card
   visually but isn't literally the card's first DOM child (e.g. a Video
   card's category tag, which follows the thumbnail) — :first-child
   above can't reach it. */
.badge-flush {
  margin-left: 0;
}
/* Live state — referenced sitewide (Home's own Live Streaming module,
   Video's .live-banner below) but never actually given its own colors
   until now; without this it was just rendering as a plain .badge. */
.badge-live {
  background: var(--brand-orange);
  border-color: var(--brand-orange);
  color: var(--brand-white);
}

/* Corner placement — anchors a badge to its card's own upper-right
   corner (e.g. a Next/Current event card's Now Streaming badge)
   instead of sitting inline with a text label. Requires a positioned
   ancestor (.module-emphasized already provides one). */
.badge-corner {
  position: absolute;
  top: 12px;
  right: 12px;
  margin-left: 0;
}
/* The sitewide button system: one structural base (.btn) plus exactly
   three color choices (.btn-orange/.btn-emerald/.btn-navy) — every
   button on the site is one of these three colors, regardless of what
   it says or does (Watch Live, Tickets, Results, Get Tickets, View
   Full Results, Log In, Send Message, Clear All, etc. all reduce to
   "which of the three colors does this action get"). Tinted wash of
   the color at rest, not a bordered outline — same low-alpha-fill
   technique as --nav-outline-hover-bg elsewhere — solid fill only on
   :hover. Purpose-specific classes (.watch-now-btn, .event-card-btn,
   .card-cta, .filter-button) still exist alongside .btn-* below, but
   only for their own LAYOUT needs (absolute positioning, filling a
   flex column, sitting at a card's corner, a disabled state) — never
   for color; adding a 4th color means adding a 4th .btn-* here, not a
   new purpose-named class. */
.btn {
  display: inline-block;
  padding: 8px 16px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  text-decoration: none;
  white-space: nowrap;
  border: none;
  cursor: pointer;
}
.btn-orange {
  background: rgba(255, 72, 0, 0.1); /* orange-500 @ 10% */
  color: var(--brand-orange);
}
.btn-orange:hover {
  background: var(--brand-orange);
  color: var(--ab-white);
}
.btn-emerald {
  background: rgba(21, 128, 61, 0.1); /* emerald-500 @ 10% */
  color: var(--tickets-btn-border);
}
.btn-emerald:hover {
  background: var(--tickets-btn-border);
  color: var(--tickets-btn-text);
}
.btn-navy {
  background: rgba(12, 34, 55, 0.1); /* navy-500 @ 10% */
  color: var(--nav-solid-bg);
}
.btn-navy:hover {
  background: var(--nav-solid-bg);
  color: var(--ab-white);
}
/* Bare icon-as-button — no frame/fill/border of its own, just a
   clickable glyph, same treatment as the footer's own social icons
   (components/footer.html: a plain <a> around the SVG, no button
   chrome). Used for Rider/Team Detail's own Follow-on-Instagram
   button, replacing that old text-button label. Resets <button>'s own
   UA chrome (border/background/padding) and its default `color:
   ButtonText`, which would otherwise stop the SVG's currentColor from
   picking up this card's own text color the way the footer's <a>
   already does for free. */
.icon-link {
  display: inline-flex;
  border: none;
  background: none;
  padding: 0;
  color: inherit;
  cursor: pointer;
}

/* "Watch Live" trigger — replaces the old Now Streaming badge
   everywhere it appears; that badge was always meant to launch the
   video player, not just announce live state. Layout only (position
   in its default standalone context) — color comes from .btn-orange,
   added alongside this in the HTML. Kept as its own class for the
   video-modal click handler, which targets it by this name sitewide. */
.watch-now-btn {
  position: absolute;
  top: 12px;
  right: 12px;
}

/* Video thumbnail — wraps an .image-placeholder so a duration pill can
   anchor to its own bottom-right corner (rather than the card's, since a
   card may also carry a category .badge up top) and a play-icon glyph
   can center over it. CSS-only glyph, not a real asset. */
.video-thumb {
  position: relative;
  margin-bottom: 8px;
}
/* Vertical-stack video cards (image above content, not .video-hero's
   side-by-side layout — this only matches .video-thumb as a direct
   child of .video-card, which .video-hero's own nested .video-thumb
   isn't) bleed the thumbnail to the card's top/left/right edges,
   canceling .module's 12px padding on those 3 sides via negative
   margin. Bottom stays at the thumb's own 8px gap before the content
   below, which keeps its normal padding untouched. */
.video-card > .video-thumb {
  margin: -12px -12px 8px;
}
.video-thumb .image-placeholder {
  aspect-ratio: 16 / 9;
  margin-bottom: 0;
}
.video-thumb::after {
  content: "\25B6";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 28px;
  color: var(--brand-white);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}
.duration-badge {
  position: absolute;
  bottom: 8px;
  right: 8px;
  background: rgba(12, 34, 55, 0.85);
  color: var(--brand-white);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 2px 6px;
}

/* Video page's Live banner — a slim strip pointing at Home's Live
   Streaming module, not a second full video-hero (that would compete
   with the Featured video below for the page's main visual weight).
   Left border in brand orange for the same "state indicator" language
   .module-emphasized already uses elsewhere (Next/Current event
   tiles), just condensed to a single row. */
.live-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  margin-bottom: 12px;
  background: var(--surface-1);
  border: 0.5px solid var(--border-strong);
  border-left: 4px solid var(--brand-orange);
  color: var(--brand-navy);
  text-decoration: none;
  cursor: pointer;
}
.live-banner:hover {
  background: var(--nav-outline-hover-bg);
}
.live-banner-text {
  flex: 1;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* Two-column / grid layout */
.grid-2 {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px;
  margin-bottom: 12px;
}
/* Grid items keep their own margin-bottom for when a .module is used
   standalone — inside a .grid-2 row, that margin would stack with the
   row's own margin-bottom and double the gap below the row, so zero it
   out here. */
.grid-2 > * {
  margin-bottom: 0;
}
/* News page grids — same stack-at-sm treatment as #event-teams above;
   every news.html grid uses .news-card.image-left (each card carries
   its own thumbnail), which leaves too little room for the headline/
   teaser text at a 2-column width on a narrow viewport. Covers Top
   Stories' own rail plus Latest News and all three topic-filtered
   buckets (Events/Riders/Teams) — same reasoning, same fix, for every
   grid on the page rather than just the one this rule originally
   shipped for. */
@media (max-width: 639.98px) {
  #top-stories-grid,
  #latest-news-grid,
  #events-grid,
  #riders-grid,
  #teams-grid {
    grid-template-columns: 1fr;
  }
}
/* video.html's own Featured videos row — a class now (every tab gets
   its own 2-up featured row, per feedback, not just Top Videos, so an
   id would collide) — stacks to a single column at the same breakpoint
   as the news.html grids above, same reasoning (each card is a full
   aspect-ratio:16/9 grab with a 36px title overlaid on it,
   .video-hero-overlay — too little room left at a 2-column width on a
   narrow viewport). */
@media (max-width: 639.98px) {
  .featured-videos-grid {
    grid-template-columns: 1fr;
  }
}
/* Winning Team/Runner-up/Top Rider — the outer container carrying
   data-winning-team-key/data-top-rider-key (include.js's
   renderRoundResultTiles queries this). It's a plain wrapper, not a
   grid itself: it holds two children, .round-result-tiles (the actual
   square-tile grid, below) and .round-result-compact (a compact
   flag/name/Time row instead, on Event Detail's own Results tab and
   Results Landing only — see the toggle further down). Every usage
   keeps its own explicit margin (inline style="margin:..." in each
   page's markup, or none) except Event Full Results
   (.full-results-round-grid), which gets none of its own — 12px here
   matches what .grid-3 used to provide by default before this became
   a plain wrapper. */
.round-result-grid {
  margin-bottom: 12px;
}
/* Winning Team / Top Rider side by side, stacking to 1 column below sm
   — Full Event Results' per-round summary and Home's Latest Results/
   League Leaders (none of these nest inside .event-rounds-grid, so the
   more specific override just below doesn't touch them). */
.round-result-tiles {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
@media (max-width: 639.98px) {
  .round-result-tiles {
    grid-template-columns: minmax(0, 1fr);
  }
}
/* Mobile only: drop Runner-up, show Winning Team + Top Rider side by
   side — 3 stacked square tiles is a lot of scroll to ask a phone for,
   and Runner-up is the one card of the three a viewer can most afford
   to lose (Winning Team and Top Rider are each a page's own hero
   result). :has() detects any .round-result-tiles actually containing
   a Runner-up (see .round-result-runner-up, added by
   include.js's roundResultTeamTile()) rather than assuming position,
   and matches .event-rounds-grid .round-result-tiles's own specificity
   exactly — placed after it here so source order breaks the tie in
   this rule's favor for Event Detail's own "always stacked" context
   too, not just Results Landing/standalone usages. This rule is moot
   below md (768px) wherever .round-result-compact takes over instead
   (shows all 3 rows regardless of width) — in practice that's every
   usage except Home's own Latest Results, which still shows tiles at
   every width, so this still matters there. Event Full Results reverts
   this rule entirely (its own override just below) since it never
   wants Runner-up dropped in the first place, at any width. */
@media (max-width: 639.98px) {
  .round-result-tiles .round-result-runner-up {
    display: none;
  }
  .round-result-tiles:has(.round-result-runner-up) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* Event Full Results keeps Runner-up at every breakpoint instead —
     unlike Results Landing/Event Detail's own Results tab, it's not
     trying to save scroll length on a phone (its own tile is already
     width-capped via .full-results-round-grid, not full page width),
     so there's no reason to drop the card here. Reverses both parts
     of the rule above, scoped 3 classes deep so it wins regardless of
     source order. */
  .full-results-round-grid .round-result-tiles .round-result-runner-up {
    display: flex;
  }
  .full-results-round-grid .round-result-tiles:has(.round-result-runner-up) {
    grid-template-columns: minmax(0, 1fr);
  }
}
/* Results Landing's per-event Rounds row, and Event Detail's Daily
   Schedule/Results rounds row (both always paired with .grid-2 in
   markup) — 2 Round blocks per page-row (Day1·Round1 | Day1·Round2 /
   Day2·Head to Head | Day3·Final), each a unified title+Team+Rider
   unit (see the <div> wrapping each Round in the markup), with that
   Round's own Winning Team/Runner-up/Top Rider trio shown 3-across.
   Stacks to 1 column below lg (1024px) — 2 Round blocks side by side
   get too narrow below that for a 3-across trio to stay legible — and
   the trio itself drops to 1 column below sm (640px), matching
   .round-result-tiles's own default stacked floor. */
.event-rounds-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  margin-bottom: 12px;
}
@media (min-width: 1024px) {
  .event-rounds-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (min-width: 640px) {
  .event-rounds-grid .round-result-tiles {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
/* Event Full Results' own tile view starts at md (768px) — see the
   compact-row toggle just below — but .round-result-tiles's own base
   rule (2-column) and .grid-3's own default (1-column below its lg/
   1024px breakpoint) are equal specificity, and .grid-3 is declared
   later in this file, so without this override the trio would render
   as a single, giant 704px-square column from 768px up to 1024px
   instead of 3-across (the exact "cards aren't scaling properly" bug
   this fixes) — same restatement Standings' own .standings-top3-tiles
   already needed for the identical reason. */
@media (min-width: 768px) {
  .full-results-round-grid .round-result-tiles {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
/* Below md (768px), Event Detail's own Results tab, Results Landing,
   Event Full Results (.event-rounds-grid and .full-results-round-grid
   respectively), and Home's own Latest Results (#home-latest-results)
   all swap the square-tile trio for a compact flag/name/Time row per
   place instead — same .mini-row component Team Standings/Rider
   Leaderboard use, just with a Winning Team/Runner-up/Top Rider role
   badge (.mini-row-badge) instead of a #1/#2/#3 rank and a Time stat
   instead of PTS. Same breakpoint everywhere it applies, so the switch
   reads as one consistent rule rather than a different cutoff per
   page — Home used to be the one usage exempted from this (it already
   sits in its own reasonably-sized column, 1fr of Home's 1fr/3fr/1fr
   row, never had the "3 squares stretch the whole page width" problem
   this solves for the others), but per feedback it should still drop
   to the same compact row on a phone, same as everywhere else. */
.round-result-compact {
  display: none;
}
@media (max-width: 767.98px) {
  .event-rounds-grid .round-result-tiles,
  .full-results-round-grid .round-result-tiles,
  #home-latest-results .round-result-tiles {
    display: none;
  }
  .event-rounds-grid .round-result-compact,
  .full-results-round-grid .round-result-compact,
  #home-latest-results .round-result-compact {
    display: block;
  }
}
/* Round label truncates to one line (e.g. "Day 2 · Head to Head ·
   1.55M") — at 4-across widths this is the longest text in the row, so
   left to wrap it grows taller than the other 3 Rounds' single-line
   labels, misaligning where every Round's own card starts underneath.
   Scoped to the label actually sitting in this row (.module-label-bar on
   Results Landing, .section-label-bar on Event Detail, both reused
   elsewhere for unrelated headers that should keep wrapping normally).
   Also zeroes .module-label-bar's own standard top margin here: each
   Round is wrapped in its own plain <div> (a grid item), and a grid
   item establishes its own formatting context, so a first-child's
   top margin can't collapse through it the way it would for a normal
   in-flow sibling — left alone, the 12px would show up as real space
   on top of whatever gap the row's own preceding element (e.g.
   .event-venue-bar) already provides, doubling it. */
.event-rounds-grid > * > p:first-child {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 0;
}

/* Insets the Daily Schedule/Results tab's "View Full Results" button
   from the page edge to match the padding of the round cards above it
   (.round-complete/.round-upcoming use 16px) — needed because the
   button sits directly in the tab panel here, not inside a padded card
   like .card-cta's other usage on Results Landing. */
.results-cta-wrap {
  padding: 0 16px 16px 0;
}

/* Three-column / grid layout (e.g. a top-3 podium callout). Stacks to a
   single column at and below md (768px) — the 3-across layout only
   applies at lg (1024px) and up — consistent with this file's
   mobile-first breakpoint convention (see below). */
.grid-3 {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  margin-bottom: 12px;
}
.grid-3 > * {
  margin-bottom: 0;
}
@media (min-width: 1024px) {
  .grid-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Standings' Top 3 (Team Standings/Rider Leaderboard) — full square
   tiles at md (768px) and up; a compact flag/name/PTS list row instead
   below that (.standings-top3-compact, further down this file). Both
   are rendered into the DOM (standings.html's own script) and toggled
   via display, not JS, so resizing across the breakpoint needs no
   re-render. This breakpoint is intentionally earlier than
   .grid-3's own lg (1024px) single-column threshold — without this
   override, the tile would still be stretching to a full, bulky
   ~400-670px square anywhere from 0 up to 1023px, well past just
   phones. */
.standings-top3-tiles {
  display: none;
  /* Capped at 800px (a fixed px ceiling, not a % — self-limiting on
     its own below this width, same --page-max-width/--content-bounds
     convention used sitewide, rather than a percentage that would
     keep shrinking the row in lockstep all the way down to an
     unreadably cramped tile) — 800px against .grid-3's own 12px gaps
     lands each tile at ~259px. Centered (margin-left/right: auto),
     matching the standings table directly below it (.standings-table-
     fixed-cols) — both cap at the same 800px and both center within
     the page, so the two read as one aligned column instead of the
     table sitting indented under a podium flush to the left edge. */
  max-width: 800px;
  margin-left: auto;
  margin-right: auto;
}
.standings-top3-compact {
  display: block;
}
@media (min-width: 768px) {
  .standings-top3-tiles {
    display: grid;
    /* .grid-3's own 3-column rule doesn't kick in until its own lg
       (1024px) breakpoint — restated here so the switch to 3-across
       actually happens at this earlier 768px point too, instead of
       showing a single huge stacked tile (worse than the compact row
       it just replaced) for the whole 768-1023px range. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .standings-top3-compact {
    display: none;
  }
}

/* Event Full Results' own per-round Winning Team/Runner-up/Top Rider
   trio (event-results-complete.html/event-results-next-current.html)
   — same bulk problem and same fix as Standings' Top 3 above: a bare
   .grid-3.round-result-grid with nothing else sharing its row
   stretches to the full page width, ballooning each tile to ~316px.
   Every OTHER .round-result-grid usage (Results Landing, Event
   Detail's own Results tab, Home's Latest Results) already nests
   inside a narrower column of its own (.event-rounds-grid's 2-up
   pairing, or Home's 1fr/3fr/1fr row) and stays a reasonable size on
   its own, so this is scoped to the one page that doesn't. Same
   800px cap; left-justified (not centered) — unlike
   .standings-top3-tiles (now centered to match the table below it),
   this page has no second, centered element of its own to align
   with. */
.full-results-round-grid {
  max-width: 800px;
}

/* Standings' Top 3, compact variant (below md/768px) — replicates the
   tile's own identity row (.rider-tile-footer/.team-tile-footer:
   flag, name, arrow) at that SAME row's own height, with the PTS stat
   folded into the same single row rather than kept as its own
   separate bar below it. 51px matches .rider-tile-footer's own
   rendered height (measured on the actual single-column tile: 15cqw
   of ~343px), not the footer+stat combined — the stat bar itself is
   dropped entirely here, its content just joins the identity row
   instead of stacking a second row underneath. No photo/logo at all —
   just rank, identity, the PTS stat, and a "view profile" arrow,
   reusing the same rank-badge colors as .rider-tile-badge-1/2/3 and
   the same sand arrow-chip treatment as .rider-tile-arrow, so it still
   reads as the same design system at a much smaller footprint. */
.mini-row {
  display: flex;
  align-items: stretch;
  height: 51px;
  border: 0.5px solid var(--border-strong);
  background: var(--ab-white);
  text-decoration: none;
  color: var(--brand-navy);
  margin-bottom: 8px;
}
.mini-row-rank {
  flex: 0 0 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 800;
}
.mini-row-rank-1 { background: var(--brand-orange); color: var(--brand-navy); }
.mini-row-rank-2 { background: var(--brand-navy); color: var(--ab-white); }
.mini-row-rank-3 { background: var(--brand-brown); color: var(--white-500); }
/* Round-result variant — a text role label (Winner/Runner-up/Top
   Rider) instead of a #1/#2/#3 rank, so this needs its own wider fixed
   width rather than .mini-row-rank's own 36px, which "Runner-up"
   wouldn't fit in. Fixed (not content-driven/auto) so all three labels
   render as the same-width block regardless of text length. Wraps (not
   nowrap) still allowed at this width in case a longer label ever
   lands here, though "Winner"/"Runner-up"/"Top Rider" all fit on one
   line today. .mini-row's own height (51px, fixed) doesn't grow to
   fit either way, and align-items:center keeps a wrapped 2-line label
   and an unwrapped 1-line one vertically centered the same way. Same 3
   colors as .mini-row-rank-1/2/3 (and .team-tile-badge-1/2/3), just
   applied to a wider chip. */
.mini-row-badge {
  flex: 0 0 76px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 4px 8px;
  font-size: 11px;
  line-height: 1.2;
  font-weight: 700;
  text-transform: uppercase;
  white-space: normal;
  text-align: left;
}
.mini-row-badge-1 { background: var(--brand-orange); color: var(--brand-navy); }
.mini-row-badge-2 { background: var(--brand-navy); color: var(--ab-white); }
.mini-row-badge-3 { background: var(--brand-brown); color: var(--white-500); }
/* 30x20 (3:2, matching .rider-tile-flag/.team-tile-flag's own
   rendered size at this component's own breakpoint boundary — 768px,
   measured at ~29x19 there) — was 24x16, visibly smaller than the
   flag size used everywhere else at a comparable width. */
.mini-row-flag {
  flex: 0 0 auto;
  align-self: center;
  margin-left: 12px;
  width: 30px;
  height: 20px;
}
.mini-row-name {
  flex: 1 1 auto;
  align-self: center;
  margin-left: 10px;
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Rider variant only (Team's own identity is just the team name, no
   second line needed) — same stacking as .rider-tile-name/
   .rider-tile-team, name on top, a smaller muted team caption tight
   beneath it, left-justified right after the flag (same as every
   other identity block on the site). */
.mini-row-name-group {
  flex: 1 1 auto;
  align-self: center;
  margin-left: 10px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* .mini-row-name's own base rule sets align-self:center, needed
   when it's a direct row child in the team-only single-line variant
   (vertical centering, in a row flex context) — nested here inside a
   COLUMN flex container instead, that same align-self:center value
   means horizontal centering instead, which was shrinking the name to
   its own text width and centering it in the group rather than
   sitting flush left like .mini-row-team does by default. Reset
   to stretch so it fills the group's width and reads left-aligned,
   matching .mini-row-team. */
.mini-row-name-group .mini-row-name {
  align-self: stretch;
  flex: 0 0 auto;
  margin-left: 0;
}
.mini-row-team {
  margin-top: 2px;
  font-size: 11px;
  font-weight: 400;
  text-transform: uppercase;
  line-height: 1;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Same stacking convention as .rider-tile-name/.rider-tile-team — the
   bigger/bolder line first (here, the PTS value itself, the
   "headline" of this stat same as a rider's own name), a smaller
   muted caption directly below it at line-height:1 with a hairline
   margin-top, not a loose gap. */
.mini-row-stat {
  flex: 0 0 auto;
  align-self: center;
  margin-left: 10px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  text-align: right;
}
.mini-row-stat-value {
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
}
.mini-row-stat-label {
  margin-top: 2px;
  font-size: 11px;
  font-weight: 400;
  text-transform: uppercase;
  line-height: 1;
  color: var(--text-secondary);
}
/* Two stats side by side (Time + Faults, Top Rider only — Faults is a
   rider-only stat, Winning Team/Runner-up stay single-stat) instead of
   .mini-row-stat's own single value/label column — same idea as
   .rider-tile-stat-dual/.rider-tile-stat-group on the full tile (a
   center divider between two equal halves), just laid out as a row
   instead of that pattern's already-horizontal bar, since
   .mini-row-stat's own default is a single vertical value/label
   stack. Compound selector (not just .mini-row-stat-dual alone) so it
   reliably wins over .mini-row-stat's own column direction. */
.mini-row-stat.mini-row-stat-dual {
  flex-direction: row;
  align-items: center;
  gap: 10px;
}
.mini-row-stat-group {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  text-align: right;
}
.mini-row-stat-group + .mini-row-stat-group {
  border-left: 0.5px solid var(--border-strong);
  padding-left: 10px;
}
/* Square 1:1, same trick as .rider-tile-arrow/.team-tile-arrow —
   align-self:stretch matches the row's own (fixed, 51px) height,
   then aspect-ratio:1/1 derives width from that instead of a fixed px
   flex-basis. Needs .mini-row's own fixed height above to
   resolve cleanly — an auto/content-driven row height here would
   leave the arrow's width (from aspect-ratio) and the row's height
   (from stretching to its tallest child, i.e. the arrow) with nothing
   to break the circular dependency, which pushes both to a default
   300x300 fallback square instead. */
.mini-row-arrow {
  flex: 0 0 auto;
  align-self: stretch;
  aspect-ratio: 1 / 1;
  width: auto;
  margin-left: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--brand-sand);
}
/* 100%, not a fixed px size — a fixed-size child gives the flex item
   itself an intrinsic content width, which wins out over
   aspect-ratio's own stretch-derived width. 100% has no intrinsic size
   of its own, so aspect-ratio is free to actually drive the box's
   width. */
.mini-row-arrow svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Four-column / grid layout (e.g. Video's topic-grouped sections).
   Same single-breakpoint stacking convention as .grid-3 above — one
   column below lg, four columns at lg (1024px) and up. */
.grid-4 {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  margin-bottom: 12px;
}
.grid-4 > * {
  margin-bottom: 0;
}
@media (min-width: 1024px) {
  .grid-4 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Unlike grid-2/3/4's single mobile-to-lg jump, grid-5's items (a small
   square thumbnail + a line of text) stay legible at a much smaller
   column width, so it scales progressively instead — 2 columns by
   default, 3 at sm, 5 at lg — shrinking and reflowing each item as the
   viewport narrows rather than holding every item at full width until
   the same single 1024px jump. */
.grid-5 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-bottom: 12px;
}
.grid-5 > * {
  margin-bottom: 0;
}
@media (min-width: 640px) {
  .grid-5 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (min-width: 1024px) {
  .grid-5 {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

/* Square identity-tile grid — the Rider/Team Directory's own
   .rider-tile/.team-tile cards (Phase 2 of the tile rollout). Same
   progressive-column idea as .grid-5 above, capped at 4 instead of 5:
   each tile carries more than .grid-5's minimal thumbnail+line (a full
   photo, flag, name, and arrow chip), so it needs more room per tile to
   stay legible. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  margin-bottom: 12px;
}
.tile-grid > * {
  margin-bottom: 0;
}
@media (min-width: 640px) {
  .tile-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (min-width: 1024px) {
  .tile-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Rider Directory's own denser variant — same progression as .tile-grid
   through 1024px, then one more step to 5 columns at 1280px (xlg, the
   very next step in this file's own sm/md/lg/xlg breakpoint scale —
   not the 1600px --page-max-width ceiling used originally, which
   skipped past xlg entirely and left the 4-column tier stretching
   across a much wider range than intended, dropping to 4 sooner than
   it should have at the 1024-1599px band). 80 riders can support a
   denser grid than Team Directory's 16 teams, which stays on the plain
   .tile-grid (caps at 4) — the two directories are sized independently
   on purpose, not through one shared class. */
.tile-grid-5 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  margin-bottom: 12px;
}
.tile-grid-5 > * {
  margin-bottom: 0;
}
@media (min-width: 640px) {
  .tile-grid-5 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (min-width: 1024px) {
  .tile-grid-5 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}
@media (min-width: 1280px) {
  .tile-grid-5 {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

/* Horizontal scrolling row (carousels). */
.carousel {
  position: relative;
  border-radius: 0;
  margin-bottom: 12px;
}
.carousel-track {
  display: flex;
  align-items: center;
  gap: 12px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}
.carousel-track::-webkit-scrollbar {
  display: none;
}
.carousel-item {
  flex: 0 0 auto;
  min-width: 160px;
  scroll-snap-align: center;
}
/* Focus item's own floor (was an inline style) — kept as the ≥640px
   size; the sm-and-below override below shrinks it so a sliver of the
   adjacent Complete/Upcoming cards peeks in on both sides instead of
   the focus card alone filling the whole viewport width. */
.carousel-item.module-emphasized {
  min-width: 200px;
}
/* Only the featured (Current) card keeps its action buttons in the
   carousel — Watch Live/Tickets/Results reads fine as the one
   highlighted item's own call-to-action row, but repeating even a
   single Results/Tickets button on every other card crowded the row
   and worked against shrinking each card's width. Full detail (and
   every state's own buttons) still lives one click away on Tour
   Calendar's own full schedule list, which this carousel already links
   to via "Full Schedule" above. */
.carousel-item:not(.module-emphasized) .event-card-actions {
  display: none;
}
/* Narrower fixed info column specifically in the carousel (vs. the
   160px used on Tour Calendar's full-width list rows) — this is a
   scrolling card, not a full-page-width row, so the same fixed width
   would eat a disproportionate share of a card this narrow. */
.carousel-item .event-card-info {
  width: 100px;
}
@media (max-width: 639.98px) {
  /* vw-based, not %: .carousel-track carries large left/right padding
     (added by the carousel's own centering script, sized off the first/
     last item's width) so its content-box % basis is unrelated to the
     viewport — sizing off it made this item's width swing wildly with
     unrelated carousel content. 100vw minus .page's 16px+16px padding
     is the actual visible width; the remaining 80px reserves 40px per
     side (minus the 12px item gap) for the small Complete/Upcoming peek. */
  .carousel-item.module-emphasized {
    min-width: 0;
    width: calc(100vw - 32px - 80px);
  }
}
/* Responsive breakpoints (Tailwind-aligned, mobile-first min-width):
   sm: 640px · md: 768px · lg: 1024px · xlg: 1280px */

/* Show only at md and above (hidden below 768px) */
.show-md-up { display: none; }
@media (min-width: 768px) {
  .show-md-up { display: inline; }
}

/* Show only at lg and above (hidden below 1024px) */
.show-lg-up { display: none; }
@media (min-width: 1024px) {
  .show-lg-up { display: inline; }
}

/* Sitewide navigation — shared nav-item spacing. No pipe/divider line
   between items — that's a deliberately separate pattern reserved for
   non-interactive grouped labels (.bar-divider, e.g. the footer's
   Privacy Policy | Terms of Use or a page-title-bar's "News | League
   Announcement"). Nav items rely on their own 12px side padding plus
   :hover's color change for separation instead, so the two patterns
   never read as the same thing for two different purposes (one
   clickable, one not). */
.nav-item { padding: 0 12px; white-space: nowrap; }
.nav-item:first-child { padding-left: 0; }
/* Right-edge counterpart to :first-child above — without it, the last
   item's own 12px right padding stacks on top of the eyebrow/main nav's
   own 16px edge padding, landing its content noticeably further from
   the edge than other bars (e.g. .page-title-bar's plain 16px). Handles
   main-nav's hamburger (reliably the last DOM child). Eyebrow nav's
   Log In / user-menu pair swap visibility rather than DOM position — see
   the dedicated #login-link rule below for that case. */
.nav-item:last-child { padding-right: 0; }
/* Log In is never the last DOM child (the user-menu span after it
   covers that via :last-child above) but it IS the visually-last item
   whenever it's the one shown, so it needs the same override directly. */
.eyebrow-nav #login-link { padding-right: 0; }
.nav-item, .dropdown-menu-item { text-decoration: none; color: inherit; }

/* Eyebrow / Utility nav — outline treatment. A direct child of <body>
   (not .page), so its border/fill is naturally full-bleed to the true
   viewport edges with no offset math needed; .eyebrow-nav-inner re-
   applies --content-bounds so the nav items' own width/position are
   unchanged from the constrained-fill version. */
.eyebrow-nav {
  height: 32px;
  background: var(--brand-white);
  font-size: 12px;
  color: var(--nav-outline-text);
}
.eyebrow-nav-inner {
  /* var(--content-edge) on BOTH sides, not max-width+margin:auto —
     that centering technique only produces the right inset while
     --content-bounds is actually narrower than .eyebrow-nav's own
     (full-bleed) width; once the viewport drops below the 1080px
     floor where --page-max-width stops shrinking, --content-bounds
     stays fixed wider than the real viewport, so max-width stops
     constraining anything and margin:auto has nothing left to center
     — the tagline collapsed to the true viewport edge instead of
     the intended content edge. --content-edge (the same value used by
     .page-title-bar/.tab-nav/.breaking-bar) degrades correctly at
     every width via its own max(16px, …) floor, so using it as
     padding here instead avoids the same trap. */
  height: 100%;
  padding: 0 var(--content-edge);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
/* Left side of the eyebrow nav — "This is Elite Sport." tagline.
   Reuses .eyebrow-tagline-lead/-emphasis for the orange-bold/
   navy-italic treatment, sized for this bar's own compact scale
   (16px). .eyebrow-nav's own fixed height (32px, independent of
   content) fits it the same way it fits the bar's own 12px base
   nav-item text. */
.eyebrow-nav-text {
  margin: 0;
  font-size: 16px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* Wraps the existing nav-item set as its own flex group so it keeps
   packing tightly together (pipe dividers, no gaps) on the bar's right
   side, now that .eyebrow-nav-inner itself is space-between to also
   fit .eyebrow-nav-text on the left. */
.eyebrow-nav-items {
  display: flex;
  align-items: center;
}
/* "This is X." treatment — "This is " in PJL Sans Bold uppercase
   (brand orange), X itself in PJL Display italic (brand navy, the
   family's only cut, so font-weight is explicit here too). Used by
   .eyebrow-nav-text's own static "This is Elite Sport." above. A
   3-phrase cycling version of this used to live in a Home-only
   .brand-bar; both that bar and its cycling-only rules have since been
   removed. */
.eyebrow-tagline-lead {
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-weight: 700;
  color: var(--brand-orange);
}
.eyebrow-tagline-emphasis {
  font-family: "PJL Display", -apple-system, sans-serif;
  font-style: italic;
  font-weight: 700;
  color: var(--brand-navy);
}
.eyebrow-nav .nav-item:hover { color: var(--nav-outline-accent); }
.eyebrow-nav .nav-item { text-transform: uppercase; }

/* Breaking News bar — Home only, sits directly below .main-nav (also a
   direct child of <body>, so this is too — naturally full-bleed with
   no calc(50% - 50vw) breakout needed, same as it). Basic layout
   replicated from Web 1.1's own .breaking-bar (orange "Breaking"
   badge, light headline panel, orange arrow), resized from that
   version's 48px to this site's standard 32px bar height — scaled
   down from its 41px badge/20px headline accordingly.
   "Edge to edge fill" + "content stays within max content width":
   since the badge and arrow are each their own solid-orange segment
   (not one bar-wide fill like .main-nav's own), each bleeds its OWN
   fill out to the true viewport edge via padding rather than the whole
   bar breaking out — badge's padding-left and arrow's padding-right
   both carry the same edge-alignment value used elsewhere (see
   .page-title-bar's own padding-right comment), so "BREAKING" and the
   arrow icon land at the normal content-bounds inset while their
   orange fill continues past that, all the way to the true edge — no
   gap of plain page background shows at either end. */
.breaking-bar {
  /* Sticky, pinned directly under .main-nav (also sticky, top:0) —
     64px is .main-nav's own rendered height (was 55.5px before that
     bar's 2026-09-10 height bump), same value/reasoning as
     .page-title-bar's own sticky top (the two never appear on the same
     page, so sharing a z-index with it is safe). */
  position: sticky;
  top: 64px;
  z-index: 29;
  display: flex;
  align-items: stretch;
  height: 32px;
  /* No border, per feedback — back to where this started before the
     top/bottom-border exploration above. Separation from .main-nav (a
     concern specifically on rollover, when the badge/arrow segments
     below swap fill color) is handled by that rollover fill itself
     now (sand-500, not navy) staying visually distinct from
     .main-nav's own navy, rather than a border line. */
  text-decoration: none;
  overflow: hidden;
}
.breaking-bar-badge {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  background: var(--brand-orange);
  color: var(--brand-navy);
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 24px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1;
  /* var(--content-edge) — lines "BREAKING" up with a module's own
     bounding-box edge, same as the eyebrow nav's tagline and the
     main-nav logo (not a module's own +16px inner text position). */
  padding: 0 8px 0 var(--content-edge);
  white-space: nowrap;
}
.breaking-bar-content {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  background: var(--surface-1);
  padding: 0 16px;
}
.breaking-bar-headline {
  color: var(--brand-navy);
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 18px;
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.breaking-bar-arrow {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  background: var(--brand-orange);
  color: var(--brand-navy);
  /* No left padding — the arrow-box (below) starts exactly where this
     segment's own orange fill starts, right at the content panel's
     edge, rather than leaving a gap of orange before it. Right padding
     matches .breaking-bar-badge's own left padding (var(--content-
     edge)) exactly, so the bar's two edge-bleed margins are the same
     size on both sides. */
  padding: 0 var(--content-edge) 0 0;
}
/* The true edge-bleed padding above can't itself be part of a square
   (it scales with viewport width) — this inner box is the actual 1:1
   square instead, sized to the bar's own 32px height so it's as large
   as it can be without exceeding the bar. */
.breaking-bar-arrow-box {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  background: var(--brand-orange);
}
.breaking-bar-arrow-box svg {
  flex: 0 0 auto;
  /* The icon's own path is already mathematically centered in its
     viewBox (verified: equal 6px gaps on all four sides of the 32px
     box at rest), but the arrowhead's converging lines concentrate
     visual "ink" on its right side versus the plain single-line shaft
     on its left — reads as off-center even though it geometrically
     isn't. Nudges it left to compensate, same optical-centering
     adjustment any directional/arrow glyph typically needs. */
  transform: translateX(-1.5px);
}
/* Rollover — scoped to just the two orange-filled segments (badge,
   arrow/arrow-box), not the whole bar: the light headline panel between
   them (.breaking-bar-content) has no fill of its own to invert, so it
   stays as-is rather than the hover picking up a color it was never
   given. Sand-500 fill per feedback (was navy, before that a brief
   swap to orange-at-rest/sand-on-rollover which was reverted) — navy
   text stays the same color throughout, since it has good contrast
   against both orange and sand. */
.breaking-bar:hover .breaking-bar-badge,
.breaking-bar:hover .breaking-bar-arrow,
.breaking-bar:hover .breaking-bar-arrow-box {
  background: var(--brand-sand);
  color: var(--brand-navy);
}

/* Account item — Log In, or the logged-in user's name toggle — reads as
   the nav's own call-to-action rather than a peer utility link like
   Tickets/Fantasy. */
.nav-item-account { font-weight: 700; }

/* Main / Primary nav — solid treatment. Sticky: stays pinned to the top
   of the viewport on scroll, unlike the eyebrow nav above it (which
   scrolls off normally, per §0 Top-Level Responsive Web Navigation). A
   direct child of <body> (not .page), so its fill is naturally
   full-bleed to the true viewport edges with no offset math needed;
   .main-nav-inner re-applies --content-bounds so nav content's own
   width/position are unchanged from the constrained-fill version. No
   margin-bottom here — .page's own top padding now supplies the gap
   before content that this used to provide when nav was .page's first
   child. */
.main-nav {
  /* Explicit height — bumped from 55.5px to 64px (2026-09-10, ported
     from Web 1.1) with no change to any element inside the bar
     (.main-nav-logo stays 38px, .main-nav-items/.nav-item keep their
     own font-size) — the extra 8.5px just becomes more top/bottom
     breathing room around them via .main-nav-inner's own
     align-items:center. Every sticky bar stacked below this one
     (.breaking-bar, .page-title-bar, .page-title, .tab-nav,
     .filter-row, .status-row, and their own further-stacked
     followers — .section-label-bar/.event-venue-bar/.leg-divider on
     Event Detail/Tour Calendar) has its own `top` offset increased by
     that same 8.5px below, since each was originally computed as this
     bar's own height plus a fixed amount — see each rule's own
     comment for the updated math. Same fixed-height technique
     .eyebrow-nav's own height already uses. */
  height: 64px;
  border: 0.5px solid var(--nav-solid-bg);
  border-bottom: none;
  border-radius: var(--radius);
  background: var(--nav-solid-bg);
  position: sticky;
  top: 0;
  z-index: 30;
}
.main-nav-inner {
  /* var(--content-edge) on BOTH sides, not max-width+margin:auto — see
     .eyebrow-nav-inner's own comment for why: that centering technique
     silently stops working below the 1080px viewport floor (margin:
     auto has nothing left to center once --content-bounds, which
     doesn't shrink past that floor, exceeds the real viewport width),
     collapsing the logo to the true viewport edge instead of the
     content edge. --content-edge avoids the same trap. */
  height: 100%;
  padding: 0 var(--content-edge);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
/* height:54px is a fixed value (not driven by the source file's own
   intrinsic size), so swapping the logo source doesn't change it or
   the bar's own overall height — width:auto just follows whatever
   aspect ratio that source has. The "_tight" logo files (no built-in
   padding around the mark, unlike the previous ones) render flush
   against .main-nav-inner's own left edge with zero gap; .main-nav-
   inner's own padding-left (above) is what now supplies the standard
   16px inset instead — same convention every other bar's own content
   uses relative to its own edge. */
.main-nav-logo {
  display: block;
  /* Stays 38px, unchanged by .main-nav's own 55.5px→64px height bump
     (2026-09-10, ported from Web 1.1 — "no resize of any main nav
     elements") — centered via .main-nav-inner's align-items:center, so
     the extra height just becomes more top/bottom breathing room
     around the logo rather than a bigger mark. */
  height: 38px;
  width: auto;
}
.main-nav-items {
  display: flex;
  align-items: center;
  font-size: 12px;
  color: var(--nav-solid-text);
  flex-wrap: wrap;
}
/* Rollover color orange-500 (2026-09-10, ported from Web 1.1; was
   --nav-solid-hover, a shared light-blue token also used by
   .text-link:hover elsewhere) — changed directly here rather than
   redefining that shared token, since .text-link's own hover treatment
   wasn't part of this request. */
.main-nav .nav-item:hover { color: var(--brand-orange); }
.main-nav-more { display: flex; align-items: center; color: var(--nav-solid-text); }
/* Main nav's own top-level labels (and the More toggle sitting alongside
   them) — PJL Display, ported from Web 1.1. font-weight/font-style are
   explicit since the family has only this one (bold italic) cut. */
.main-nav .nav-item,
.main-nav-more {
  font-family: "PJL Display", -apple-system, sans-serif;
  font-style: italic;
  font-weight: 700;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* More menu and User menu share one dropdown style regardless of which
   nav (main or eyebrow) they're toggled from. Dropdown popups are always
   a light surface, so they use the outline text/border regardless of
   whether their trigger nav is solid or outline. */
.main-nav .dropdown-menu,
.eyebrow-nav .dropdown-menu {
  border-color: var(--nav-outline-border);
  color: var(--nav-outline-text);
}
.main-nav .dropdown-menu-divider,
.eyebrow-nav .dropdown-menu-divider {
  border-top-color: var(--nav-outline-divider);
}
.main-nav .dropdown-menu-item:hover,
.eyebrow-nav .dropdown-menu-item:hover {
  background: var(--nav-outline-hover-bg);
}

/* Sm-and-below nav collapse — replaces both the eyebrow nav and the
   "More" dropdown with a single hamburger opening a full-screen sheet
   (every destination flattened into one list, see main-nav.html). Only
   .nav-hamburger's own display flips per breakpoint; .main-nav-items
   and .eyebrow-nav going display:none removes them from layout
   entirely, so .main-nav's own space-between naturally settles to
   logo | hamburger with no extra rules needed. */
.nav-hamburger {
  display: none;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 8px;
  color: var(--nav-solid-text);
  cursor: pointer;
}
@media (max-width: 639.98px) {
  .nav-hamburger {
    display: flex;
  }
  .main-nav-items,
  .eyebrow-nav {
    display: none;
  }
}

/* Full-screen sheet + backdrop — position:fixed so both escape .page's
   own max-width and cover the true viewport. The sheet covers the full
   viewport (inset:0) and slides in from off-screen-right; the backdrop
   is mostly redundant once the sheet is fully in (nothing shows behind
   it) but kept per spec for the brief moment mid-transition and as a
   click target to close. */
.nav-sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(2, 22, 42, 0.85);
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s, visibility 0.2s;
}
.nav-sheet-backdrop.open {
  opacity: 1;
  visibility: visible;
}
/* Dark theme (2026-09-10, ported from Web 1.1 — was --surface-1/white)
   — navy-500 fill throughout, matching .main-nav's own dark treatment
   instead of a light sheet. .nav-sheet-logo already uses the orange
   logotype (not the navy one, see main-nav.html), so it needs no swap
   to stay visible against this fill. */
.nav-sheet {
  position: fixed;
  inset: 0;
  z-index: 1001;
  background: var(--brand-navy);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform 0.2s;
}
.nav-sheet.open {
  transform: translateX(0);
}
/* padding: 17px 16px (2026-09-10) — top/bottom set to keep this
   header's own overall rendered height unchanged (~72px) now that
   .nav-sheet-logo below is 38px instead of the 56px it briefly was:
   that was a same-day 8px 16px, sized for the 56px logo (56 + 8+8 =
   72) — restoring the logo to 38px alone would have shrunk the header
   to 38 + 8+8 = 54px as a side effect, which feedback said wasn't
   wanted (logo size was the only thing meant to change). 17px each
   side gets back to the same 72px total (38 + 17+17 = 72). Left/right
   stay explicitly 16px, matching .nav-sheet-item's own left padding
   (below) so the logo's left edge and the menu items' text both start
   at the same inset. */
.nav-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: 0 0 auto;
  padding: 17px 16px;
  border-bottom: 0.5px solid var(--nav-solid-divider);
}
/* 38px (2026-09-10; was 56px, briefly, then back down here) — matches
   .main-nav-logo's own size exactly, per feedback, without growing
   this header (a same-day 56px attempt did the opposite, forcing the
   header taller to fit it). Height alone wasn't enough to actually
   match, though: main-nav.html's own markup was still pointing this
   img at PJL_Logotype_RGB_Orange.svg, the non-"_tight" file, which
   carries built-in padding around the mark the same way the old
   .main-nav-logo source used to (see THAT rule's own comment,
   styles.css) — at an identical 38px box height, the padded file
   renders its actual mark visibly smaller and inset from the box's own
   edges, unlike the flush-crop "_tight" file. Switched the markup to
   PJL_Logotype_RGB_Orange_tight.svg (main-nav.html) to fix that, so
   the rendered mark itself, not just the img box, now matches
   .main-nav-logo's.
   Placement matches too: .nav-sheet-header's own 16px left padding is
   the same effective inset .main-nav-inner's own
   padding-left:var(--content-edge) resolves to at every viewport width
   this mobile sheet is ever shown at (--content-edge is a flat 16px
   below the 767.98px breakpoint — see that variable's own override —
   comfortably covering the ≤639.98px range where .nav-hamburger/
   .nav-sheet exist at all), so the two logos land at the same left
   edge despite one being set with a variable and the other a literal.
   Also still matches .nav-sheet-item's own 16px left padding below,
   same as before this size change. width:auto keeps the mark's native
   aspect ratio either way. */
.nav-sheet-logo {
  display: block;
  height: 38px;
  width: auto;
}
.nav-sheet-close {
  font-size: 28px;
  line-height: 1;
  background: none;
  border: none;
  color: var(--white-500);
  cursor: pointer;
  padding: 0;
}
.nav-sheet-items {
  display: flex;
  flex-direction: column;
  padding: 8px 0;
}
/* PJL Sans/bold (2026-09-10; was the sitewide default sans-serif at
   regular weight — a same-day sand-500 color attempt was reverted, per
   feedback, in favor of this font-family+weight change instead). Color
   stays --nav-solid-text; hover below still switches to navy against
   its own light hover fill, unaffected by this change. */
.nav-sheet-item {
  display: block;
  padding: 14px 16px;
  font-size: 15px;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--nav-solid-text);
  text-decoration: none;
}
.nav-sheet-item:hover {
  background: var(--nav-solid-hover);
  color: var(--brand-navy);
}
.nav-sheet-item-divider {
  border-top: 0.5px solid var(--nav-solid-divider);
  margin-top: 8px;
  padding-top: 22px;
}

/* Footer — fixed solid navy bar, independent of the main/eyebrow nav
   swap above. A direct child of <body> (not .page), so its fill is
   naturally full-bleed to the true viewport edges; .footer-nav-inner
   re-applies --content-bounds so the footer content's own width/
   position are unchanged from the constrained-fill version. */
.footer-nav {
  border: 0.5px solid var(--footer-bg);
  border-radius: var(--radius);
  background: var(--footer-bg);
  font-size: 12px;
  color: var(--footer-text);
}
.footer-nav-inner {
  /* var(--content-edge) left/right, not max-width+margin:auto — see
     .eyebrow-nav-inner's own comment for why that centering technique
     alone breaks below the 1080px viewport floor. Top/bottom padding
     unrelated, unchanged. */
  padding: 12px var(--content-edge) 32px;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}
/* Social icons — left-aligned (not spread/centered like the old 3-way
   layout), top-aligned against the taller copy block beside them. */
.footer-nav-icons {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex: 0 0 auto;
}
/* Copyright/legal copy — stacked one line per sentence (flex column on
   the copy block's own children, rather than <br> tags, so line gap is
   controlled by `gap` alone) instead of one run-on paragraph. Only the
   copyright statement itself (the three <strong> lines) is bold;
   Privacy Policy stays regular weight since it's a distinct action, not
   part of the legal notice. */
.footer-nav-copy {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.footer-nav-icons a,
.footer-nav-copy a {
  color: inherit;
  text-decoration: none;
}
/* Privacy Policy | Terms of Use pipe — .bar-divider's own default
   (currentColor, i.e. this bar's light footer-text) reads too bright
   against the navy fill; matches .main-nav's own pipe color instead,
   since both bars now share the same navy fill. */
.footer-nav-copy .bar-divider {
  border-left-color: var(--nav-solid-divider);
}
.footer-nav-icons a:hover,
.footer-nav-copy a:hover {
  color: var(--footer-hover);
}

/* Bottom Navigation: grouped secondary nav, distinct from Footer.
   Three sections, each a header + a vertical link list. Colors match
   the eyebrow nav's outline treatment; layout/typography unchanged. A
   direct child of <body> (not .page), so its top border/fill stretches
   full-bleed to the true viewport edges; .bottom-nav-inner re-applies
   --content-bounds so the section grid's own width/position are
   unchanged from the constrained-fill version. */
.bottom-nav {
  background: var(--brand-navy);
  border-top: 0.5px solid var(--nav-outline-border);
}
.bottom-nav-inner {
  /* Self-contained padding (var(--content-edge) both sides), not
     max-width+margin:auto — that centering technique CENTERS a
     narrower box inside this full-bleed bar rather than pinning it to
     the left, so past --page-max-width's 1600px ceiling (where
     --content-bounds stops growing but .bottom-nav keeps stretching
     with the viewport) the grid drifted off the left content edge
     instead of hugging it like every other bar. Padding pins it flush
     left unconditionally, same technique as .eyebrow-nav-inner/
     .main-nav-inner/.footer-nav-inner. */
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
  /* No more margin-bottom on .bottom-nav itself — bottom-nav now sits
     flush against .footer-nav (both navy) with no gap between them.
     This bottom padding replaces that removed gap as internal space
     instead, so the navy fill (not the page background) shows through
     it. */
  padding: 16px var(--content-edge) 12px;
}
/* The seam between .bottom-nav and .footer-nav (both navy) — content-
   width only (inset by --content-edge on both sides, matching this
   grid's own padding), not full-bleed like every other bar's own
   divider, using the same divider color as .main-nav's own pipe
   dividers between nav items. A pseudo-element instead of a plain
   border-bottom on .bottom-nav-inner itself: now that this element has
   no max-width of its own (see above), a real border would stretch
   full-bleed with it instead of stopping at content-width. */
.bottom-nav-inner::after {
  content: "";
  position: absolute;
  left: var(--content-edge);
  right: var(--content-edge);
  bottom: 0;
  border-bottom: 1px solid var(--nav-solid-divider);
}
.bottom-nav-title {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  /* Matches .main-nav-items' own text color — same solid-navy-fill
     token set, now that this bar's fill is navy too, not the
     outline-treatment's light fill. */
  color: var(--nav-solid-text);
  margin: 0 0 8px;
}
.bottom-nav-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 12px;
  color: var(--nav-solid-text);
}
.bottom-nav-links a {
  color: inherit;
  text-decoration: none;
}
.bottom-nav-links a:hover {
  color: var(--nav-solid-hover);
}

/* Module categories — color-only overlays from ../pjl-brand/pjl-color-palette.md.
   Combine with .module / .module-emphasized (e.g.
   class="module cat-editorial") wherever a module needs a category color.
   Reusable sitewide, not Home-specific.
     Editorial / Content Aggregation (+ Core) -> Navy
     Commerce                                -> Emerald
     Partnerships                            -> Amber
   Promotional Cross-Links intentionally has no category class here — its
   color is contextual to each link's target, decided per instance.
   Child .th/.ts/.ts-muted text needs style="color:inherit" to pick up the
   category color, matching the existing .module-emphasized convention. */
:root {
  --cat-editorial-bg: var(--navy-25);
  --cat-editorial-border: var(--navy-500);
  --cat-editorial-text: var(--navy-700);

  --cat-commerce-bg: var(--emerald-25);
  --cat-commerce-border: var(--emerald-500);
  --cat-commerce-text: var(--emerald-700);

  --cat-partnerships-bg: var(--amber-25);
  --cat-partnerships-border: var(--amber-500);
  --cat-partnerships-text: var(--amber-700);
}
.cat-editorial {
  background: var(--cat-editorial-bg);
  border-color: var(--cat-editorial-border);
  color: var(--cat-editorial-text);
}
.cat-commerce {
  background: var(--cat-commerce-bg);
  border-color: var(--cat-commerce-border);
  color: var(--cat-commerce-text);
}
.cat-partnerships {
  background: var(--cat-partnerships-bg);
  border-color: var(--cat-partnerships-border);
  color: var(--cat-partnerships-text);
}

/* Click/tap-to-open dropdown */
.dropdown-toggle {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
}
/* .dropdown-toggle's own `font: inherit` shorthand would otherwise reset
   .nav-item-account's bold back to the nav's normal weight (same
   specificity, later in the cascade) — restate it here at higher
   specificity so the logged-in user toggle stays bold like Log In.
   text-transform is restated for a different reason: it isn't part of
   the `font` shorthand at all, but browsers' own UA stylesheet sets
   text-transform:none on <button> specifically, breaking this button's
   inheritance from .eyebrow-nav .nav-item's uppercase even though the
   sibling Log In <a> (no such UA override) inherits it fine. */
.dropdown-toggle.nav-item-account {
  font-weight: 700;
  text-transform: uppercase;
}
.dropdown-menu {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 8px;
  background: var(--surface-1);
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 8px 0;
  min-width: 200px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
  z-index: 40; /* above .main-nav's sticky z-index: 30, so the eyebrow nav's User menu isn't hidden behind it */
  text-align: left;
}
.dropdown-menu.open { display: block; }
/* PJL Sans, ported from Web 1.1's submenu-link treatment — including its
   explicit text-transform:none, since a dropdown here nests inside its
   trigger's own .nav-item span and would otherwise inherit that item's
   uppercase treatment (main-nav's top-level labels, and now the eyebrow
   nav's). */
.dropdown-menu-item {
  display: block;
  padding: 8px 16px;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-weight: 700;
  text-transform: none;
}
/* Main nav's own More menu — its items double as that overflowed set of
   main-nav top-level labels, so they match the same uppercase treatment
   those labels carry in the bar itself, unlike every other dropdown
   (eyebrow's user menu) which keeps the plain-case default above. */
#more-menu .dropdown-menu-item {
  text-transform: uppercase;
}
.dropdown-menu-divider { border-top: 0.5px solid var(--border); margin-top: 4px; padding-top: 8px; }
/* User Menu's name header row — a static label, not an action, so it
   keeps the item's padding/alignment but drops the hover affordance
   (.eyebrow-nav .dropdown-menu-item:hover) that real menu actions get. */
.dropdown-menu-name {
  font-weight: 700;
  color: var(--brand-navy);
  cursor: default;
}
.eyebrow-nav .dropdown-menu-name:hover {
  background: none;
}

/* New page-title style — prototype, NOT a replacement for
   .page-title-bar below (that stays exactly as-is structurally/
   visually; its own text just moved out into this element instead).
   Now rolled out to EVERY page that has a .page-title-bar — each one
   duplicates that bar's own former text into a .page-title directly
   after it, and the bar itself is left empty (just its orange accent
   strip). This element is now the page's actual visible heading, and
   — per that rollout — has taken over .page-title-bar's old role as
   the "default gap before whatever comes next" provider; .page-title-bar's
   own margin-bottom is 0 unconditionally now, since it always has this
   element directly after it.
   Exploratory per feedback: no bar/chrome of its own beyond its own
   fill (below), so it just sits in the page's own normal flow (unlike
   .page-title-bar's sticky/full-bleed/fixed-height bar treatment) —
   deliberately minimal to start, expect this to change as the new
   style gets established (this comment covers the ROLLOUT +
   MARGIN-ARCHITECTURE state only; visual styling below is still
   whatever the last round of feedback landed on).
   Desktop font-size/line-height started matching news-item-hero.html's
   own .article-hero-headline exactly (72px) — the initial reference
   point for "what size this should read at" — but has since diverged
   smaller per feedback, first to 48px then to 36px; the mobile step
   (≤639.98px, same breakpoint that rule uses) diverged further still,
   down to 32px rather than that page's own 50px, and was untouched by
   the 48px→36px desktop change. Uppercase added per feedback too.
   font-style:normal uses PJL Display's own upright Bold cut
   (assets/fonts/PJLDisplay-Bold.otf, added specifically for this — see
   this file's own @font-face block up top for why the family's
   original italic-only cut couldn't just be de-italicized via CSS).
   Sticky — top:84px is .main-nav's 64px + .pjl-rail-compact's own 20px
   (was 96px = 64 + .page-title-bar's own 32px, before that class was
   replaced sitewide by .pjl-rail-compact on 2026-09-10), so it picks
   up immediately where that (also sticky) bar ends,
   same slot/z-index-below-it convention every other sticky bar in this
   file uses.
   Fill matches .filter-row's own actual color (var(--surface-0)) per
   feedback — "the search bar" means that row/bar itself, not
   .filter-search (the text FIELD inside it, a different, lighter
   color) — an earlier pass got this wrong twice: first using
   --sand-500 (the color .filter-row was temporarily given to check
   ITS OWN bounds), then .filter-search's own color instead of
   .filter-row's. The fill covers the
   WHOLE element now too — padding-top/bottom (16px each) instead of
   margin, so the 16px of breathing room above/below the text is
   PAINTED (part of this box), not empty space outside it the way
   margin would leave it. margin:0 is explicit and load-bearing here —
   this is a <p>, and dropping the margin declaration entirely (rather
   than zeroing it) let the browser's own default <p> margin (1em top
   and bottom, i.e. a full extra 48px/32px at this element's own
   font-size) silently reapply on top of the padding, a real bug
   confirmed via computed style (marginTop/marginBottom both read
   "48px" until this was added) — not anything actually requested.
   Without a fill at all, page content scrolling
   directly behind the transparent text read as the title itself
   "moving" (a static, correctly-pinned element next to visibly-shifting
   content behind it reads as relative motion) — solid fill (now
   covering the full box) hides that scrolling content the same way
   every other sticky bar's own fill already does.
   Switching margin→padding here means the gap-absorption trick
   .page-title + .tab-nav/.filter-row/.status-row used to do (cancel
   THIS element's margin-bottom via collapsing, then re-add the same
   16px as their OWN padding) no longer applies — there's no external
   margin left to cancel, since the 16px now lives inside this element's
   own (filled) padding-bottom instead. Those three rules dropped both
   the margin-top:-16px cancellation AND their own compensating
   padding-top — adding it back would double the gap to 32px.
   Their sticky top offsets DID still need to move, though — margin-top
   on a sticky element turns out NOT to add to its own `top` offset the
   way normal in-flow margin would (a stuck element's box still lands
   exactly at `top`, margin or not), but padding DOES expand the box
   those numbers are measured from, since it's now inside the box
   itself rather than outside it. Currently 155.6px/151.2px (padding-
   top is 24px as of 2026-09-10, was 16px — padding-bottom stays 8px;
   both +8px over their prior 147.6/143.2 from that change); each
   follower's own comment shows the exact arithmetic and re-measure
   this same way if padding-top/bottom here ever changes again. */
.page-title {
  position: sticky;
  top: 84px;
  z-index: 28;
  margin: 0;
  background: var(--surface-0);
  /* Full-bleed fill per feedback — same width:100vw + margin-left:
     calc(50% - 50vw) breakout .page-title-bar/.tab-nav/.filter-row
     already use, with padding-left/right reapplying --content-edge so
     the actual headline text still sits at the normal content inset;
     only the fill itself reaches the true viewport edges now. Was
     previously a plain in-flow block matching .filter-row's own
     content-width bounds (per earlier feedback) — that's reversed now. */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding-left: var(--content-edge);
  padding-right: var(--content-edge);
  padding-top: 24px;
  padding-bottom: 8px;
  font-family: "PJL Display", -apple-system, sans-serif;
  font-weight: 700;
  font-style: normal;
  font-size: 36px;
  line-height: 1.1;
  text-transform: uppercase;
  color: var(--brand-navy);
}
/* Bottom-rule variant — .tab-nav/.filter-row/.status-row each already
   carry their own border-bottom:0.5px solid var(--border-strong) (their
   own base rules), so a page-title-fill-to-sticky-bar transition
   already gets a dividing line for free, right at that bar's own edge
   — and since those bars are full-bleed (100vw), so is their line.
   Everywhere else (plain content following .page-title — most pages),
   there's no such line at all; this gives .page-title the same rule,
   for exactly those cases, so the title area reads consistently
   closed-off either way. :not(:has(+ .X)) for each of the three sticky
   bars rather than a single positive selector, since "whatever isn't
   one of these specific three" is the actual condition — matches this
   file's existing :has() convention (e.g. .page-title-bar:has(.page-title-back))
   rather than inventing a different pattern for the inverse case.
   Full-bleed via a plain left:0/right:0 ::after pseudo-element — now
   that .page-title's own box is full-bleed too (width:100vw breakout,
   per feedback, see that rule's own comment above), the pseudo-element
   just needs to span its containing block's full width, not bleed
   further out past it the way it had to when .page-title's own box was
   still at normal content width (this rule's previous version used
   left/right: calc(-1 * var(--content-edge)) for exactly that reason —
   no longer needed now that the parent itself already reaches the true
   viewport edge). */
.page-title:not(:has(+ .tab-nav)):not(:has(+ .filter-row)):not(:has(+ .status-row)) {
  margin-bottom: 16px;
}
/* margin, not padding — the fill/box itself should still end flush at
   the divider line below (unchanged, at the existing padding-bottom
   edge); this just opens plain page-background space AFTER that line,
   before whatever content follows. Uses margin instead of the
   gap-absorption (negative-margin + padding) pattern the sticky-to-
   sticky cases below use, because that pattern exists specifically to
   avoid a page-background "flash" between two independently-sticky
   siblings during scroll — content here isn't sticky, so a plain
   margin gap is simplest and reads correctly. */
.page-title:not(:has(+ .tab-nav)):not(:has(+ .filter-row)):not(:has(+ .status-row))::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  border-bottom: 0.5px solid var(--border-strong);
}
@media (max-width: 639.98px) {
  .page-title {
    font-size: 32px;
  }
}

/* Dark variant — ported from Web 1.1 (2026-09-10). Style name:
   .page-title-dark (paired with .page-title on the same element, e.g.
   class="page-title page-title-dark"). navy-500 fill, white-500 text
   (--brand-navy/--white-500), replacing the light --surface-0 fill/
   navy text default above — for a page whose header zone (rail +
   title, e.g. .pjl-rail-compact sitting above it) is itself dark, so
   the title continues that same darker palette instead of switching
   back to a light fill directly underneath it. */
.page-title.page-title-dark {
  background: var(--brand-navy);
  color: var(--white-500);
}
/* Bottom-rule variant restored for the dark case, same as the light
   .page-title:not(:has(+ .tab-nav))...::after rule above — color
   matched to .tab-nav-dark's own border-bottom-color
   (--nav-solid-divider) rather than the light variant's
   --border-strong, so a dark page WITH a tab bar and one WITHOUT read
   as the same divider treatment, just relocated (this rule for the
   no-sticky-follower case, .tab-nav-dark's own border for the
   has-tab-nav case). Same specificity as the base rule above (one
   class + three :not() + ::after each) — wins by appearing later in
   source order. */
.page-title-dark:not(:has(+ .tab-nav)):not(:has(+ .filter-row)):not(:has(+ .status-row))::after {
  border-bottom: 0.5px solid var(--nav-solid-divider);
}

/* Ported from Web 1.1's styles.css — contextual tab nav (below). This
   file's own .pjl-rail-compact (see that rule's own comment) now fills
   the "universal section/page title bar" role a dedicated
   .page-title-bar class used to (2026-09-10 — that class, and its own
   .page-title-bar a / *:last-child / :first-child companion rules, are
   removed sitewide in favor of it; see .pjl-rail-compact's own comment
   for the sticky/full-bleed treatment carried over). Selectors below
   are self-contained (the placeholder module uses a compound selector
   precisely so it wins regardless of source order), so appending here
   is safe. */

/* Pipe-style divider between two non-interactive labels that sit
   together on one side (e.g. a page-title-bar's "News | League
   Announcement", or the footer's Privacy Policy | Terms of Use) rather
   than pushed to opposite ends via a bar's own justify-content:
   space-between. Deliberately not used in any nav-item list — see
   .nav-item's own comment — so a pipe always means "grouped label,"
   never "clickable nav item." Uses currentColor so it matches whichever
   bar it's placed in without a dedicated color token. */
.bar-divider {
  padding-left: 12px;
  margin-left: 12px;
  border-left: 1px solid currentColor;
}

/* 40px full-bleed navy-500 band directly above .pjl-rail (2026-09-10,
   ported from Web 1.1 — style name: .footer-rail-lead). A genuinely
   SEPARATE element, not padding added onto .pjl-rail itself — Web 1.1
   tried that first and hit a real bug: a painted padding band sharing
   .pjl-rail's own `background` property inherited whatever color the
   rail itself was, so a later change to the rail's own bleed color
   changed the band too, unintentionally. This band's own navy-500 is
   hardcoded independently instead, so it can't drift if .pjl-rail's
   own bleed ever changes again. Flush against .pjl-rail below it (no
   gap specified) — same width:100vw breakout technique as every other
   full-bleed bar on the site. */
.footer-rail-lead {
  height: 40px;
  background: var(--brand-navy);
  width: 100vw;
  margin-left: calc(50% - 50vw);
}

/* .pjl-rail / .pjl-rail-compact — new brand design element, two
   variants of the same bar. .pjl-rail-compact is NOT a scaled-down
   .pjl-rail: every column keeps the exact width it has in the
   standard rail — only the bar's own height drops to 20px, and the
   logo graphic (too tall to fit at that height without rescaling,
   which the spec doesn't want) is simply omitted rather than shrunk.
   So --rail-x/--logo-clear below are fixed constants derived once
   from the STANDARD rail's 40px height, shared unchanged by both
   variants — never recomputed from .pjl-rail-compact's own 20px.
   Full-bleed/content-edge convention: the bar's own box breaks out to
   the true viewport edge via the calc(50% - 50vw) technique (valid
   wherever it sits, whether directly in .page's flow or nested deeper
   — .page's symmetric padding keeps its content box centered on the
   true viewport center at every level, so this technique resolves
   correctly either way), while its content stays padded in to
   --content-edge, landing at the same left/right edge as every other
   module/bar on the site.
   --rail-x is the rail's own base unit — "the width of the orange
   block with the PJL logo" per spec. See .pjl-rail-logo's own comment
   for its derivation (a second, unrelated "x" — the PJL mark's own
   descender width — feeds into it). */
.pjl-rail,
.pjl-rail-compact {
  /* Always the standard rail's own 40px, per the comment above — NOT
     var of each variant's own (different) height. --logo-clear is a
     manual bump over the clear-space spec's own derived ~2.51px (see
     .pjl-rail-logo's own comment for that math) — 4px reads as a
     touch more comfortable around the mark, per design feedback.
     --logo-h/--rail-x both flow from it (mark height = block height
     minus its own clearance twice, mark width from that height via
     the mark's fixed 150:46 aspect ratio, --rail-x = mark width plus
     clearance twice again) so bumping --logo-clear alone keeps
     everything else — including every other slot's own width, since
     they're all sized off --rail-x — correctly in proportion. */
  --logo-clear: 4px;
  --logo-h: calc(40px - 2 * var(--logo-clear));
  --rail-x: calc((var(--logo-h) * 150 / 46) + (2 * var(--logo-clear)));
  /* Bleed fill (full-bleed margins outside the content area — the true-
     viewport-edge overflow past --content-edge) is set per variant just
     below, NOT shared here — both .pjl-rail and .pjl-rail-compact are
     orange-500 (2026-09-10, ported from Web 1.1). This is independent
     of .pjl-rail-slot--flex's OWN fill (the center gap between the
     fixed blocks, below) — that slot carries its own explicit
     white-500 background regardless of the bar's own bleed color, same
     as Web 1.1's source. */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  display: flex;
  align-items: center;
  padding-left: var(--content-edge);
  padding-right: var(--content-edge);
}
.pjl-rail {
  height: 40px;
  background: var(--brand-orange);
}
/* .pjl-rail-compact now also fills .page-title-bar's old role sitewide
   (2026-09-10) — the universal sticky bar directly under .main-nav,
   replacing that now-removed class everywhere it appeared. Sticky/
   z-index/first-child treatment below is carried over unchanged from
   .page-title-bar's own (top:64px = .main-nav's own rendered height;
   z-index one below .main-nav's own 30). Unlike .page-title-bar, this
   bar has no text/label/back-link capacity of its own — any page that
   relied on the old in-bar back-link (Rider/Team Profile, some News
   items) drops it sitewide per feedback, a referral-link model to be
   revisited separately. */
.pjl-rail-compact {
  height: 20px;
  background: var(--brand-orange);
  position: sticky;
  top: 64px;
  z-index: 29;
}
/* Closes the gap above the bar when it's the page's very first piece of
   content (main-nav sits directly above it, with nothing in between):
   pulls the bar up over .page's own 16px top padding so it sits flush
   against main-nav, same treatment .page-title-bar used to carry.
   Scoped to :first-child so pages where something else legitimately
   comes first keep their normal gap. */
.page > .pjl-rail-compact:first-child {
  margin-top: -16px;
}
/* The logo graphic can't fit a 20px-tall bar at the standard scale
   above, and the spec calls for dropping it rather than rescaling —
   see this rule block's own top comment. The block itself keeps its
   --rail-x width and orange fill either way, so the column still
   lines up with the standard rail's; only the img is removed. */
.pjl-rail-compact .pjl-rail-logo img {
  display: none;
}
/* Content-area slots per the rail's own layout spec: x | Flex | x |
   0.25x | x — a fixed-width lead slot (.pjl-rail-logo, below), one
   slot that grows to fill whatever space the fixed slots don't use,
   then two more fixed slots at the rail's own --rail-x width and one
   at 0.25 of it. .pjl-rail-slot--sm is kept as its own class (rather
   than a width override on .pjl-rail-slot) so it reads as "the 0.25x
   slot" directly at the markup level. */
.pjl-rail-slot,
.pjl-rail-slot--flex,
.pjl-rail-slot--sm {
  /* .pjl-rail's own align-items:center sizes flex children to their
     own content height otherwise — with none yet, every slot would
     render at 0 height. The spec's diagram shows these as full-height
     blocks (same as .pjl-rail-logo, which sets this itself), so that's
     the structural default here too. */
  height: 100%;
}
.pjl-rail-slot {
  flex: 0 0 var(--rail-x);
  background: var(--brand-orange);
}
.pjl-rail-slot--flex {
  /* The spec's own neutral gap between the fixed blocks — own explicit
     white-500 fill (2026-09-10, ported from Web 1.1), independent of
     .pjl-rail/.pjl-rail-compact's own bleed background above. Used to
     carry no fill of its own (the bar's own shared white-500 background
     just showed through), which worked fine back when both the bleed
     and this slot were the same white-500 — now that both .pjl-rail and
     .pjl-rail-compact's own bleed is orange-500 while this slot stays
     white-500, it needs its own explicit declaration instead of
     inheriting the bar's. Side effect on both variants: since
     .pjl-rail-slot's own fixed-slot fill (below) is the same orange-500
     as the bar's own bleed, those lead/trailing slots now blend into
     it — only this flex-gap slot and the sand-500 quarter-slot read as
     distinct blocks against the bleed. */
  flex: 1 1 auto;
  background: var(--white-500);
}
.pjl-rail-slot--sm {
  flex: 0 0 calc(var(--rail-x) * 0.25);
  background: var(--brand-sand);
}
/* The rail's lead "x" slot: an orange block containing the navy PJL
   mark (PJL_Logotype_RGB_Navy_tight.svg — same "_tight" flush-crop
   file .main-nav-logo uses), sized per a clear-space spec separate
   from the rail's own x/Flex/0.25x layout above: the mark is inset by
   0.25 of ITS OWN "x" on all four sides, where that inner "x" is the
   width of the P's descender — the diagonal leg's flat foot where it
   meets the baseline (the `h13.18` segment in the SVG's own path
   data, out of the mark's 150-wide/46-tall viewBox — see the navy SVG
   itself). Solving for the render scale that makes the padded box's
   height equal the standard rail's 40px gives, in viewBox units:
     denominator = 46 (mark height) + 2 × 0.25 × 13.18 (top+bottom clearance) = 52.59
     clearance (one side) = 40px × (0.25 × 13.18) / 52.59 ≈ 2.51px
   --logo-clear (above) rounds that up to a flat 4px by hand for a bit
   more breathing room (see that rule's own comment) — --logo-h/
   --rail-x are then derived from --logo-clear rather than restating
   this math a second time, so they stay correct if --logo-clear ever
   moves again. box-sizing: border-box + padding: var(--logo-clear)
   leaves an inner content box of exactly --logo-h by construction —
   the mark keeps its native 150:46 aspect ratio, so the img itself
   just fills that box at width/height: 100% with no further math
   needed. Only .pjl-rail is 40px tall, so only there does this box's
   height match its own bar exactly; see .pjl-rail-compact
   .pjl-rail-logo img above for the 20px bar. */
.pjl-rail-logo {
  flex: 0 0 var(--rail-x);
  box-sizing: border-box;
  height: 100%;
  padding: var(--logo-clear);
  background: var(--brand-orange);
  display: flex;
}
.pjl-rail-logo img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Standalone filter row — a set of filter controls in their own row,
   e.g. above a card list or table (Rider Directory: Search, Team,
   Country). .filter-group bundles Team/Country/Clear as ONE flex item
   in .filter-row so they wrap together rather than splitting apart from
   each other. No divider between Search and the group — each control
   already carries its own border, which is enough separation on its
   own; a rule line between them read as more of an old toolbar
   convention than something this bar needed. Below the mobile
   breakpoint, Search and .filter-group are forced onto their own
   full-width rows (rather than left to .filter-row's own organic
   flex-wrap point, which shifts with content and wouldn't reliably land
   the two on separate rows).
   Styled to match .tab-nav — same slot directly under .page-title-bar
   (Rider/Team Directory have a filter row instead of tabs there, never
   both), so it picks up the same treatment: sticky at the same top
   offset, a full-bleed bottom rule while the controls themselves stay
   at the normal content width, and no explicit height of its own — like
   .tab-nav, height comes from its content (the filter controls' own
   padding, ~32px) rather than a fixed value, so the mobile breakpoint's
   2-row wrap below can still grow it without being clipped.
   padding-bottom:12px (2026-09-10; was 8px) is this row's own baseline
   breathing room before its border-bottom below — a same-day padding-
   top:12px attempt (meant to fix .filter-row-dark's own fields reading
   cramped against .page-title-dark's identical navy-500 fill, no color
   break between them to lean on) was reverted per feedback, "too much
   space between title text and filters" — back to no padding-top, the
   space above the fields comes entirely from .page-title's own fill
   ending flush against this row. Independent of whatever precedes it
   either way (this used to be bundled into the now-removed .page-title
   + .filter-row gap-absorption override as half of a top/bottom split,
   but that pairing was only ever about relocating .page-title's own
   margin — see git history if that context is ever needed again). */
.filter-row {
  position: sticky;
  top: 84px;
  z-index: 28;
  background: var(--surface-0);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  justify-content: flex-end;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding-left: var(--content-edge);
  padding-right: var(--content-edge);
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 0.5px solid var(--border-strong);
}
/* No margin/padding compensation needed here anymore — .page-title's
   own 16px trailing space is PADDING now (filled, part of its own box;
   see its own comment, styles.css), not an external margin this row
   ever needed to cancel-then-relocate. This row just starts flush
   against it.
   top:155.6px overrides .filter-row's own plain 84px — now that
   .page-title is sticky too, this row has to stack BELOW it, not at
   the same slot. 155.6px = 84 (.pjl-rail-compact's own stuck bottom)
   + 71.6 (.page-title's own total rendered height at its 36px desktop
   font — 39.6 content + 24px padding-top + 8px padding-bottom; was
   63.6 with the pre-2026-09-10 16px padding-top) —
   measured directly on riders.html, not computed from font metrics
   alone (line-height math on a real font is imprecise). Mobile
   override just below uses the equivalent measurement at .page-title's
   own 32px mobile font (35.2 content + 24 + 8 = 67.2 total, on top of
   the same 84px base) — mobile font-size is unaffected by desktop's
   own 48px→36px change, so that value hasn't moved independent of the
   padding-top change. */
.page-title + .filter-row {
  top: 155.6px;
}
@media (max-width: 639.98px) {
  .page-title + .filter-row {
    top: 151.2px;
  }
}
/* Dark variant (2026-09-10) — style name: .filter-row-dark (paired with
   .filter-row on the same element, e.g. class="filter-row
   filter-row-dark"). navy-500 fill, divider color matched to
   .tab-nav-dark's own border-bottom-color (--nav-solid-divider) rather
   than the light variant's --border-strong, same convention as every
   other dark bar sitewide. The search/select fields themselves
   (.filter-search/.filter-select, below) need no dark-specific
   treatment — they already carry their own white-500 fill regardless
   of the row's own background, same as a search field sitting on any
   other colored bar. */
.filter-row.filter-row-dark {
  background: var(--brand-navy);
  border-bottom-color: var(--nav-solid-divider);
}
/* Clear All (riders.html) DOES need its own override, though —
   .btn-navy's translucent-navy-on-white fill (and .filter-button's own
   :disabled navy-500 border, below) both assume a light row bg for
   contrast. Against this row's own solid navy-500 fill, translucent
   navy nearly disappears into it, and the disabled state's navy-500
   border is literally the same color as the fill — both effectively
   invisible. Reuses --nav-solid-* tokens instead, same light-on-navy
   convention .tab-nav-dark/.nav-sheet's own dark treatment already use. */
.filter-row-dark .btn-navy {
  background: rgba(255, 255, 255, 0.1);
  color: var(--nav-solid-text);
}
.filter-row-dark .btn-navy:hover {
  background: var(--nav-solid-text);
  color: var(--brand-navy);
}
.filter-row-dark .filter-button:disabled {
  color: var(--nav-solid-text-muted);
  border-color: var(--nav-solid-divider);
}
.filter-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
@media (max-width: 639.98px) {
  .filter-search,
  .filter-group {
    flex: 1 1 100%;
  }
  /* Team/Country/Clear All lean right here, same as the row itself
     does above the breakpoint (.filter-row's own justify-content) —
     .filter-group otherwise defaults to flex-start once it's a
     full-width row of its own. */
  .filter-group {
    justify-content: flex-end;
  }
}
.filter-select,
.filter-search {
  font-size: 12px;
  font-weight: 400;
  color: var(--nav-outline-text);
  background: var(--ab-white);
  border: 0.5px solid var(--nav-outline-border);
  padding: 8px 12px;
  /* Explicit — <select>/<input>/<button> each render at a slightly
     different height under identical padding/font-size/box-sizing
     (measured: select 32.5px, search 31px, button 30px), a native
     cross-element font-metric quirk, not anything this file's own
     rules control. Locking all three to the same value (see
     .filter-button's own matching height below) is what actually
     lines them up in the row, same "explicit height rather than
     letting padding drive it" approach .page-title-bar/.eyebrow-nav/
     .breaking-bar already use for their own cross-browser consistency. */
  height: 32px;
}
/* Focus outline: covered by the sitewide a/button/input/select rule
   near the top of the file — this bar's light background means the
   default navy applies with no override needed here. */
/* Unstyled <select> sizes itself to its longest OPTION, not its current
   value — with the full country list as options, that stretched Country
   to ~225px regardless of viewport. Capping width (plus ellipsis for
   the closed, collapsed state) keeps both selects a predictable size. */
.filter-select {
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Clear All — styled like the site's other navy buttons instead of
   matching the plain outline treatment its sibling select/search
   controls use, so it reads as an action rather than another filter
   input (via .btn/.btn-navy added alongside it in the HTML — this
   class itself is now just the JS hook, height match with its row
   siblings, and the :disabled treatment below). Inactive until there's
   actually a search or filter set to clear — toggled via the button's
   own `disabled` attribute (see riders.html's applyFilters(), which
   sets it from the same anyActive check driving the results
   themselves) rather than a class, so it's never clickable while
   inactive either. */
.filter-button {
  /* Same explicit-height reasoning as .filter-select/.filter-search's
     own comment just above — .btn's own padding renders this a
     different height than its row siblings otherwise. */
  height: 32px;
}
.filter-button:disabled {
  color: var(--text-muted);
  background: none;
  /* Every other state this button can be in (enabled tint, hover fill)
     reads fine borderless — but with background:none here too, a
     disabled Clear All had no visible edge at all against the row's
     white fill. Matches .filter-select/.filter-search's own border
     weight/color exactly, per feedback, so it reads as "another
     disabled field in this row" rather than inventing a new bordered-
     button treatment nothing else on the site uses. */
  border: 0.5px solid var(--nav-outline-border);
  cursor: default;
}
/* Grows to take the row's leftover space instead of sitting fixed-width
   like the selects/button beside it — a search box reads best when it
   has room to type in, not just enough for its placeholder. */
.filter-search {
  flex: 1 1 auto;
  min-width: 120px;
  font-family: inherit;
}
.filter-search::placeholder {
  color: var(--text-muted);
}

/* General-purpose call-to-action layout — real <button>/<a> in most
   places, or a <span> where it sits inside a card that's already a
   link (e.g. "View Full Results" on a Results event tile), since
   nesting interactive elements inside the card's own <a> is invalid
   HTML. Layout only (corner placement within its card) — color comes
   from whichever .btn-* is added alongside it in the HTML. */
.card-cta {
  /* Block + fit-content + margin-left:auto pushes it to the card's
     lower-right corner while staying in normal flow (as the card's
     last child) — avoids the overlap risk of position:absolute against
     a card whose text content height varies. */
  display: block;
  width: fit-content;
  margin-top: 8px;
  margin-left: auto;
}

/* A card title that's a link (e.g. an event name linking to Event
   Details) — reset it to read like plain text, not .card-cta below it. */
.th a {
  color: inherit;
  text-decoration: none;
}

/* Placeholder-sized module — for a page with a single content block
   standing in for not-yet-designed content. Combine with .module
   (e.g. class="module module-placeholder"). */
.module.module-placeholder {
  min-height: 600px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

/* Status row / events overview — the same 3-card Latest | Next-or-
   Current | Next-or-Upcoming cycle in two contexts: Tour Calendar's
   sticky .status-row above its schedule list, and Home's plain
   .events-overview among its other normal modules (see .status-row's
   own remaining rule below for the full state logic). Cards inside
   follow the same state treatment (module-complete / module-emphasized
   / module-upcoming) as the schedule below.
   Shared here is everything that should change in both places at once:
   the featured-column split (1fr/1.5fr/1fr — narrower than an even
   1/3 each, but not as dominant as the earlier 2fr), an 8px gap
   between cards, the width:100vw + margin-left:calc(50% - 50vw) +
   padding:var(--content-edge) edge-to-edge technique, the --white-50
   fill, and the border-bottom closing off that band before whatever
   comes next (a leg-divider on Tour Calendar, the next Home module
   here). Each selector's own rule right after this one holds only
   what's genuinely different between the two contexts. */
.status-row,
.events-overview {
  display: grid;
  grid-template-columns: 1fr 1.5fr 1fr;
  gap: 8px;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding-left: var(--content-edge);
  padding-right: var(--content-edge);
  background: var(--white-50);
  border-bottom: 0.5px solid var(--border-strong);
}
/* Sticky, same slot/top-offset as .tab-nav and .filter-row (Tour
   Calendar has this instead of either directly under .page-title-bar).
   padding-bottom/margin-bottom (rather than the shared rule's own
   padding-left/right alone) replaces the space an hr.divider used to
   provide directly below this module — padding, not margin, so the
   fill/border extend down through it instead of leaving a gap of plain
   page background between the cards and the border. */
.status-row {
  position: sticky;
  top: 84px;
  z-index: 28;
  padding-bottom: 16px;
  margin-bottom: 12px;
}
/* No margin/padding compensation needed here anymore — .page-title's
   own 16px trailing space is PADDING now (filled, part of its own box;
   see its own comment, styles.css), not an external margin this row
   ever needed to cancel-then-relocate. This row just starts flush
   against it.
   top:155.6px overrides .status-row's own plain 84px (base rule,
   above) — same reasoning and same measured values as .page-title +
   .filter-row's identical override (styles.css, see its own comment
   for how 164.3/146.7 were derived). */
.page-title + .status-row {
  top: 155.6px;
}
@media (max-width: 639.98px) {
  .page-title + .status-row {
    top: 151.2px;
  }
}
/* Plain in-flow grid, without .status-row's sticky-under-the-nav-stack
   treatment, since this sits among Home's other normal, non-sticky
   modules rather than pinned above a long scrolling schedule list. */
.events-overview {
  padding-top: 16px;
  padding-bottom: 16px;
}
/* Each column is just its own card now — the state label used to be a
   separate eyebrow line above it (.module-label-bar); it's since moved
   inside, as this card's own .event-card-info/.event-card-state, same
   placement as the full schedule list below. .status-col still wraps
   it (rather than dropping straight to the card) so the grid's own
   items stay simple flex columns — harmless with a single child, and
   keeps this ready to carry a second element again without restructuring
   if a future need calls for it. */
.status-col {
  display: flex;
  flex-direction: column;
}
.status-col > *:last-child {
  margin-bottom: 0;
  flex: 1;
}
/* Below sm, the overview keeps only the featured (Current) card —
   Complete/Next hide rather than squeeze into a 3-across row that's
   already tight at desktop width. Same :has() scoping as the orange
   label above, so it keeps following Current automatically as the
   cycle rotates. Collapsing the grid to a single track lets that
   remaining card fill the row's full width instead of sitting in its
   original 2fr slot with empty space on both sides. */
@media (max-width: 639.98px) {
  .status-row,
  .events-overview {
    grid-template-columns: 1fr;
  }
  .status-col:not(:has(.module-emphasized)) {
    display: none;
  }
}
/* Plain bold-navy-uppercase module header row (Latest News, Latest
   Results, Latest Videos, Partners, Tour Calendar's Previous/Next/
   Upcoming, Results Landing's Round headers, etc.) — matches
   .section-label-bar's typography without its solid navy fill.
   Formerly two near-identical classes (.status-label-bar, a flex
   space-between row for a header + right-aligned action; and
   .status-label, the same treatment without the flex row for headers
   with no action link) — consolidated into this one class since a
   flex row with a single child renders identically to a plain block
   for one line of text, so the distinction was never visually
   necessary.
   Carries its own 24px top margin (double the site's standard 12px
   block spacing — a deliberately larger gap since this bar is also a
   section break, e.g. "Latest News"/"Our Partners"/each Round header)
   so it controls the gap above itself instead of relying on whatever
   precedes it to supply the right amount — collapses with a normal
   in-flow sibling's own
   margin-bottom rather than stacking, but does NOT collapse when this
   is the first child of a padded/bordered container (e.g. a grid
   item's own box), so a first-child usage may need margin-top:0
   locally if that extra 12px isn't wanted there. */
.module-label-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin: 24px 0 8px;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-weight: 700;
  color: var(--brand-navy);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.module-label-bar .section-label-bar-action {
  text-transform: none;
}

/* Generic horizontal rule between page sections (e.g. status row and
   the full schedule below it). */
.divider {
  border: none;
  border-top: 0.5px solid var(--border-strong);
  margin: 16px 0;
}

/* Article footer — Contact Detail / Follow The PJL (components/
   article-footer.html), appended below every News Item article's own
   body. No background/border chrome (removed per feedback) — reads as
   plain running text continuing the page, not a boxed callout; padding
   dropped too since there's no longer a box edge for it to hold text
   away from. Each link below carries .link-orange directly (styles.css)
   rather than a component-specific rule of its own — the sitewide
   default link style. */
.article-footer {
  margin-top: 12px;
}
.article-footer-heading {
  font-size: 14px;
  font-weight: 700;
  color: var(--brand-navy);
  margin: 0 0 4px;
}
.article-footer-heading:not(:first-child) {
  margin-top: 12px;
}

/* Standings-type table — ranked rows (team or rider) sharing one
   generic wireframe table style; column set is content-TBD wherever it's
   used, only the row-count/structure is meaningful. */
.standings-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  margin-bottom: 12px;
  /* Same fill as the top-3 tiles' own name row above this table
     (.team-tile-footer/.rider-tile-footer, both var(--ab-white)) —
     --surface-1 is that same white, so the ranked list below reads as
     the same surface as the tiles instead of the page's own grey. */
  background: var(--surface-1);
  /* Same border every card on the site uses (.module, .team-tile,
     .rider-tile: 0.5px solid var(--border-strong)) — the table reads
     as one more card in that family instead of a borderless block. */
  border: 0.5px solid var(--border-strong);
}

/* A long ranked list (e.g. Rider Results, 48 rows) shown alongside a
   shorter one (e.g. Team Results, 16 rows) initially renders only its
   top 15, with the rest revealed by .show-more-toggle — avoids nesting
   a second scroll region inside a page that already scrolls. The
   toggle lives in the round's tab-nav (not inside the table, and not
   added height below it), sized/padded to match .tab-item so it sits
   at the same row height instead of growing the tab-nav; margin-left:
   auto pushes it to the row's far end, away from the two tabs. Shown
   only while Rider Results is the active list (wired alongside the
   tab switch in the page script). */
.extra-row { display: none; }
table[data-expanded="true"] .extra-row,
tbody[data-expanded="true"] .extra-row { display: table-row; }

.show-more-toggle {
  padding: 8px 16px;
  margin-left: auto;
  margin-bottom: -0.5px;
  font-size: 12px;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
}

/* Condenses a table's rank number (and a placeholder image, when the
   row has one) together with the ranked entity's name into a single
   cell/column, rather than a separate leading Rank/Placement column. */
.ranked-cell {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Fixed width (not auto) so a 2-digit rank (10+) doesn't push the logo
   and name over relative to the single-digit rows above it — up to 80
   rows on the Rider Leaderboard, so 2 digits is the max this needs to
   cover. */
.ranked-cell .rank-num {
  flex: 0 0 20px;
  text-align: right;
  font-weight: 700;
  font-size: 16px;
  /* Same color every rank number gets, top 3 included — no separate
     muted-grey/inherit split by row anymore (see the removed top-3
     .rank-num override further down this file). */
  color: inherit;
}
.standings-table th,
.standings-table td {
  text-align: left;
  padding: 8px 12px;
  border-bottom: 0.5px solid var(--border);
}
.standings-table td a {
  color: inherit;
  text-decoration: none;
}
/* Team Standings only — a heavier, brand-navy divider below rank 12
   (between it and 13), replacing that row's own plain hairline
   (.standings-table td's shared border-bottom above) to mark the cut
   line between the two groups. */
.tab-panel[data-panel="team"] .standings-table tbody tr:nth-child(12) td {
  border-bottom: 2px solid var(--navy-500);
}
.standings-table th {
  font-weight: 600;
  color: var(--text-primary);
  border-bottom: 0.5px solid var(--border-strong);
}
/* Rollover state — rows will eventually link to Team / Rider detail pages */
.standings-table tbody tr:hover {
  background: var(--nav-outline-hover-bg);
  cursor: pointer;
}
.standings-table th:first-child,
.standings-table td:first-child {
  text-align: right;
}
/* Right-aligning the first column above assumes it's a plain numeric
   Rank (e.g. Fantasy's standalone Rank column). Where rank has been
   condensed into the Team/Rider name column instead (.name-first-col),
   that first column is a name, not a number, and reads better left. */
.standings-table.name-first-col th:first-child,
.standings-table.name-first-col td:first-child {
  text-align: left;
}
/* Marks any other numeric column (PTS, Winnings, a time, a count) so it
   right-justifies like every other number in the table, instead of
   sharing the table's plain left default meant for names/text. Applied
   directly on the th/td rather than picked off by column position,
   since column order differs table to table (Team Standings, Rider
   Leaderboard, Team/Rider Results). */
.standings-table .col-num {
  text-align: right;
}
/* Fixed column widths — Team Standings and Rider Leaderboard (this
   modifier, standings.html) otherwise fall back to plain table-layout:
   auto, which sizes every column independently off that table's own
   widest cell. With two different column counts (4 vs 5) and unrelated
   content driving each, their PTS/Winnings/Country widths drift apart
   table to table with no visible logic tying them together. Both
   tables share the same first four columns in the same order (Name,
   PTS, Winnings, Country) — Rider Leaderboard just appends a 5th
   (Team) — so fixing widths by position keeps PTS/Winnings/Country the
   same width in both, while table-layout:fixed hands whatever's left
   over to the one column with no explicit width (Name), rather than
   needing that computed and set explicitly too. Capped at 800px (the
   same cap Standings' own Top 3 podium and Event Full Results already
   use) rather than stretching to the page's full column width — at
   full width, fixed percentage columns just inflate into oversized
   gaps around short content (a 3-letter country, a $ amount) instead
   of reading as a tighter, denser table. Centered (margin-left/right:
   auto) rather than flush against the page's left edge, so the leftover
   space splits evenly on both sides at full width instead of collecting
   as one lopsided gap on the right. */
.standings-table.standings-table-fixed-cols {
  table-layout: fixed;
  max-width: 800px;
  margin-left: auto;
  margin-right: auto;
}
/* Wraps just the table (standings.html — not the Top 3 tiles/compact
   rows above it, which already have their own mobile treatment) in a
   horizontal-scroll container. This table's percentage columns each
   hold a hard pixel floor that doesn't shrink (a 40px headshot, a 20px
   rank number, "United Arab Emirates" or "Vantage Point Equestrian" at
   this font) — below roughly 800px total, those floors stop adding up
   to their own percentage's share, and cell content that's
   white-space:nowrap (unlike a normal paragraph, a table cell doesn't
   reflow to fit) starts rendering past its own column's edge, visually
   overlapping the column next to it rather than the row just wrapping
   taller. Locking the table to its full 800px and scrolling instead
   keeps every column at the width it actually needs, same fix
   .tab-nav (styles.css) already uses for its own "doesn't fit AND
   can't be made to fit" row of tabs. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.table-scroll .standings-table.standings-table-fixed-cols {
  min-width: 800px;
}
/* PTS/Winnings narrowed from an original 12%/18% (both had 45-60px of
   dead space beyond their actual widest value, "240"/"$402,781") to
   free up room for Country below without shrinking Team Standings'
   own Team-name column, which is sized off these three (this table's
   auto 4th/5th column, see the fixed-cols comment above) — 12+18+15
   and 8+13+24 both total 45%, so Team's own width is unchanged even
   though Country's grows. */
.standings-table.standings-table-fixed-cols th:nth-child(2),
.standings-table.standings-table-fixed-cols td:nth-child(2) {
  width: 8%;
}
.standings-table.standings-table-fixed-cols th:nth-child(3),
.standings-table.standings-table-fixed-cols td:nth-child(3) {
  width: 13%;
}
/* Widened from 15% — several full country names ("United Arab
   Emirates", "United Kingdom", both used on the Rider Leaderboard;
   "United Kingdom" on Team Standings too) wrapped to 2 lines at 15%,
   breaking row-height consistency the same way Team's own name column
   did (see this table's own fixed-cols comment above). 24% comfortably
   fits "United Arab Emirates", the longest value across both rosters. */
.standings-table.standings-table-fixed-cols th:nth-child(4),
.standings-table.standings-table-fixed-cols td:nth-child(4) {
  width: 24%;
}
.standings-table.standings-table-fixed-cols th:nth-child(5),
.standings-table.standings-table-fixed-cols td:nth-child(5) {
  width: 15%;
}
/* Rider Leaderboard only (not Team Standings, which shares this same
   class/first-4-columns but has no 5th/Team column) — several team
   names ("Ironbridge Jumping Club", "Vantage Point Equestrian") wrapped
   to 2 lines in the shared 15% Team column at the table's 800px full
   width, breaking row-height consistency row to row. Narrows the Rider
   name column (40% auto down to 30% — still comfortably fits the
   longest rider name, rank number, and headshot) and hands the freed
   10% to Team. Scoped via the tab-panel wrapper since both tables
   share identical classes with nothing else to tell them apart. */
.tab-panel[data-panel="rider"] .standings-table-fixed-cols th:first-child,
.tab-panel[data-panel="rider"] .standings-table-fixed-cols td:first-child {
  width: 30%;
}
.tab-panel[data-panel="rider"] .standings-table-fixed-cols th:nth-child(5),
.tab-panel[data-panel="rider"] .standings-table-fixed-cols td:nth-child(5) {
  width: 25%;
}
/* Team Standings only — unlike Rider Leaderboard (explicit 30% Name
   column above), Team's own Name column was left as the table's one
   "auto" column, so it absorbed the full 55% (440px) left over once
   PTS/Winnings/Country's shares were taken out — far more than any
   team name actually needs (~260px including headshot+rank; longest
   is "Vantage Point Equestrian"). Pixel widths here (not %, avoiding
   an auto-then-percentage circular calculation) size Name to what it
   needs instead, same PTS/Winnings/Country pixel values the shared
   rule above already gives Rider Leaderboard at 800px, just paired
   with a narrower Name (the leftover 140px spread across the other
   columns by the browser's own table-layout:fixed distribution).
   width/min-width/max-width all match the shared 800px floor/cap
   (.table-scroll and .standings-top3-tiles above) rather than the
   660px this rule used to force — that 660px broke the "tiles and
   table both cap at 800px and read as one aligned column" intent
   .standings-top3-tiles' own comment describes, leaving Team
   Standings' table narrower than the podium tiles above it on any
   viewport wide enough to show both at full size. Higher specificity
   than .table-scroll's own min-width rule above (an extra tab-panel +
   attribute selector) — redundant now that both resolve to the same
   800px, kept for when a future change to one no longer matches the
   other. */
.tab-panel[data-panel="team"] .table-scroll .standings-table.standings-table-fixed-cols {
  width: 800px;
  min-width: 800px;
  max-width: 800px;
}
.tab-panel[data-panel="team"] .standings-table-fixed-cols th:first-child,
.tab-panel[data-panel="team"] .standings-table-fixed-cols td:first-child {
  width: 300px;
}
.tab-panel[data-panel="team"] .standings-table-fixed-cols th:nth-child(2),
.tab-panel[data-panel="team"] .standings-table-fixed-cols td:nth-child(2) {
  width: 64px;
}
.tab-panel[data-panel="team"] .standings-table-fixed-cols th:nth-child(3),
.tab-panel[data-panel="team"] .standings-table-fixed-cols td:nth-child(3) {
  width: 104px;
}
.tab-panel[data-panel="team"] .standings-table-fixed-cols th:nth-child(4),
.tab-panel[data-panel="team"] .standings-table-fixed-cols td:nth-child(4) {
  width: 192px;
}
/* Rollover state — links to a Rider, Team, or News detail page, or (for
   .video-card) opens the video overlay instead of navigating. No color
   reset here: every real usage is paired with a category class
   (.cat-editorial etc.) that already sets an explicit color, and
   since that class is declared earlier in this file, a same-
   specificity `color: inherit` here would win the cascade and
   silently override it instead of falling back to it. */
.rider-card,
.team-card,
.news-card,
.video-card {
  display: block;
  text-decoration: none;
}
.rider-card:hover,
.news-card:hover,
.video-card:hover,
.team-card:hover {
  background: var(--nav-outline-hover-bg);
  cursor: pointer;
}
/* Article/video blocks drop .module's border and .cat-editorial's
   navy-25 fill — plain, unbordered tiles instead. Scoped to just these
   two (not .rider-card/.team-card, which keep the standard .module
   treatment) since only News/Video asked for this. The hover wash above
   is untouched — that's interaction feedback, not the resting fill. */
.news-card,
.video-card {
  border: none;
  background: none;
}
/* Headline bold, teaser (.ts) left at its own normal weight — every
   News/Video card's headline sitewide (Home, News, Video, Rider
   Detail's Related content), including the hero/featured item (nested
   .card-with-headshot.hero-story/.video-hero still lands inside one of
   these two), so this one rule covers every instance without touching
   .th's own base rule (used broadly elsewhere, e.g. Tickets/Fantasy/
   Store card titles, for things that aren't a News/Video headline and
   shouldn't turn bold along with these). Some featured items already
   set font-weight:700 inline for other reasons (matching a larger
   font-size bump) — redundant now, harmless. */
.news-card .th,
.video-card .th {
  font-weight: 700;
}
/* Latest News / Related Content feed rows (.news-card.image-left —
   Home's Latest News, News Landing's Latest News, Rider Detail's
   Related Content) sit stacked directly against each other with no
   card border between them, so an uneven row height (a badge wrapping
   to a 2nd line at a narrow width, or a headline/teaser that runs long
   enough to wrap an extra line) reads as a visible seam/background-band
   mismatch against the rows above and below, not just a taller card.
   Clamping all three to a fixed line count keeps every row the same
   height regardless of copy length. Scoped to .image-left specifically
   — .hero-story's own badge/.th/.ts (the lead story) intentionally show
   full-length copy, unclamped. */
.news-card.image-left .badge {
  white-space: nowrap;
}
.news-card.image-left .th {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.news-card.image-left .ts {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* line-clamp only caps the max at 2 lines — a 1-line teaser still
     collapses to its own shorter height, so two rows in the same grid
     row (e.g. a 1-line teaser beside a 2-line one) render at different
     heights even though both are "default" cards. Reserving the full
     2-line height always (measured directly: 14px .ts renders at 16px
     for 1 line / 32px for 2, same "measure, don't calculate" approach
     used for this file's sticky offsets) keeps every card the same
     height regardless of how much of the clamp an individual teaser
     actually uses. */
  min-height: 32px;
}
/* Two canonical news-item styles, not per-instance inline styles —
   "default" (.news-card.image-left, above) and "featured" (.hero-story,
   below). color:inherit and the 8px-over-the-badge .th margin used to
   be repeated inline on every single card (~19 on News alone, plus
   Home's own Latest News); consolidating here means every instance
   automatically matches the other, rather than relying on each new
   card's markup copying the right inline style correctly. */
.news-card.image-left .th,
.news-card.image-left .ts {
  color: inherit;
}
.news-card.image-left .th {
  margin: 8px 0 4px;
}
.hero-story .th,
.hero-story .ts {
  color: inherit;
}
.hero-story .th {
  font-size: 18px;
  margin: 8px 0 4px;
}
/* Timestamp forced to the sitewide muted-text token, not whatever the
   card's own category color is — every instance sets color:inherit
   (needed so the rest of the card's text picks up e.g. .cat-editorial's
   navy instead of some other default), which for THIS one element
   fights the "subdued" look .ts-muted's own base rule already gives
   every other muted caption on the site (--text-muted, a grey,
   vs. .cat-editorial's own navy-700 text color it was otherwise
   inheriting). !important overrides that inline style rather than
   editing the ~15 individual occurrences it's set on across every page
   that has a News/Video card. */
.news-card .ts-muted,
.video-card .ts-muted {
  color: var(--text-muted) !important;
}

/* Generic modal overlay — shared by the video-playback placeholder
   (video.html) and the Register/Log In modal (secondary-nav.html), any
   in-page lightbox that shouldn't navigate to a separate page. */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.modal-overlay[hidden] {
  display: none;
}
.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(2, 22, 42, 0.85);
}
.modal-content {
  position: relative;
  width: 100%;
  max-width: 800px;
}
.modal-close {
  position: absolute;
  top: -40px;
  right: 0;
  font-size: 28px;
  line-height: 1;
  color: var(--brand-white);
  background: none;
  border: none;
  cursor: pointer;
}
.video-frame {
  width: 100%;
  aspect-ratio: 16 / 9;
  margin-bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--brand-navy);
  border: 0.5px solid var(--brand-white);
  color: var(--brand-white);
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* Register / Log In modal — narrower than the default .modal-content,
   and a real functional-looking card (not .module's own placeholder
   treatment), since a form should read as functional rather than as a
   wireframe stand-in. */
.auth-card {
  background: var(--brand-white);
  padding: 24px;
  max-width: 360px;
  margin: 0 auto;
}
/* Reuses the sitewide .tab-item for the actual tab look (color,
   uppercase, active-state navy underline — same as Standings/About
   PJL/every other .tab-nav row) rather than a second, drifted-looking
   tab style scoped to just this modal. Not .tab-nav itself on the
   container, though: that class is sticky and full-bleed
   (width:100vw breakout) for sitting under a page's own title bar,
   both wrong inside a centered, fixed-width modal — .auth-tabs stays
   its own lightweight container (flex row, gap to match .tab-nav's
   own spacing between content-width items, bottom border) instead. */
.auth-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  border-bottom: 0.5px solid var(--border-strong);
}
/* .tab-item is normally an <a> (no native chrome to undo); paired here
   with a <button>, which needs its own browser-default appearance
   reset before .tab-item's own styling can show through cleanly. */
.auth-tab-btn {
  background: none;
  border: none;
  font-family: inherit;
  cursor: pointer;
}
.auth-form[hidden] {
  display: none;
}
.auth-form label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--brand-navy);
  margin-bottom: 12px;
}
.auth-form input {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 8px 10px;
  font-size: 13px;
  font-family: inherit;
  border: 0.5px solid var(--border-strong);
}
.auth-form .card-cta {
  width: 100%;
  text-align: center;
  margin: 8px 0 0;
}

/* Generic content-page form (e.g. Feedback, Contact) — a real,
   functional-looking card rather than .module's own placeholder-TBD
   treatment, reusing .auth-form's label/input pattern but sized for a
   full page section instead of a narrow modal, and extended with
   textarea/select for longer-form submissions. */
.form-card {
  background: var(--surface-1);
  border: 0.5px solid var(--border-strong);
  padding: 24px;
  max-width: 480px;
  margin: 0 auto 32px;
}
.form-card label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--brand-navy);
  margin-bottom: 16px;
}
.form-card input,
.form-card textarea,
.form-card select {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 8px 10px;
  font-size: 13px;
  font-family: inherit;
  border: 0.5px solid var(--border-strong);
  background: var(--ab-white);
}
.form-card textarea {
  resize: vertical;
  min-height: 200px;
}
.form-card .card-cta {
  width: 100%;
  text-align: center;
  margin: 8px 0 0;
}

/* Card / module layout with a placeholder image beside the text —
   headshot (rider) or logo (team), same box treatment either way. */
.card-with-headshot {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* Every OTHER card pairing a headshot/logo with text — Standings' and
   Home League Leaders' top-3 (bare .round-complete, no label), the
   Rider/Team Directory and Team Profile Roster cards, the Rider/Team
   Identity blocks, and each Event's per-team competing-riders cards —
   gets the same content-left/image-right/image-fills-height/1:1 square
   treatment already established for Winning Team/Top Rider, generalized
   here onto .card-with-headshot itself (the one wrapper every case
   shares) rather than each's own differently-shaped outer card. That's
   also why this is scoped to :not(...the labeled-round-complete
   descendant...): Winning Team/Top Rider already bleed against their
   OUTER .round-complete specifically, so their image can span past the
   label line above .card-with-headshot, which a .card-with-headshot-
   anchored version of this rule can't do (and shouldn't try to —
   applying both to the same element would just conflict). Excludes
   tables' own bare <span> placeholders by construction: those were
   never wrapped in .card-with-headshot to begin with. */
.card-with-headshot:has(> .headshot-placeholder):not(.round-complete:has(> p:first-child) .card-with-headshot, .top3-card .card-with-headshot, .image-left .card-with-headshot),
.card-with-headshot:has(> .logo-placeholder):not(.round-complete:has(> p:first-child) .card-with-headshot, .top3-card .card-with-headshot, .image-left .card-with-headshot) {
  position: relative;
  overflow: hidden;
}
.card-with-headshot:has(> .headshot-placeholder):not(.round-complete:has(> p:first-child) .card-with-headshot, .top3-card .card-with-headshot, .image-left .card-with-headshot) > div,
.card-with-headshot:has(> .logo-placeholder):not(.round-complete:has(> p:first-child) .card-with-headshot, .top3-card .card-with-headshot, .image-left .card-with-headshot) > div {
  min-width: 0;
}
/* width/height:auto so the sitewide default (40px) and .lg (96px)
   sizes don't win — include.js measures this element's own (now
   content-driven) height at runtime and sets width to match, the same
   square-via-JS approach already used for Winning Team/Top Rider (see
   that rule's own comment for why aspect-ratio:1/1 alone isn't
   reliable here), and pads .card-with-headshot's own right side to
   match so the text never runs under the now-larger image. */
.card-with-headshot:has(> .headshot-placeholder):not(.round-complete:has(> p:first-child) .card-with-headshot, .top3-card .card-with-headshot, .image-left .card-with-headshot) > .headshot-placeholder,
.card-with-headshot:has(> .logo-placeholder):not(.round-complete:has(> p:first-child) .card-with-headshot, .top3-card .card-with-headshot, .image-left .card-with-headshot) > .logo-placeholder {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: auto;
  height: auto;
}

/* .image-left — applied to the OUTER card (e.g. Rider Profile's
   Identity module, or a News feed row), not .card-with-headshot
   itself: the photo bleeds flush against that outer card's own edges
   (past its 16px padding), same "bleed against the outer bordered
   card" technique as Winning Team/Top Rider above (see that rule's own
   comment) — just mirrored to the LEFT edge here since the photo reads
   first. .card-with-headshot in between stays a plain, unpositioned
   flex row (excluded from the generalized rule above via the
   .image-left .card-with-headshot argument), so the placeholder's
   containing block skips it and attaches directly to this outer
   .image-left card instead — otherwise the image would only fill
   .card-with-headshot's own inset box, leaving the outer card's
   padding as a visible gap around it. padding-left (reserving room for
   the text) is likewise set on this outer card, not on
   .card-with-headshot. Covers .image-placeholder too (News feed rows)
   — unlike headshot/logo placeholders, it keeps its own aspect ratio
   (4/3, via a data-bleed-ratio attribute the markup sets) instead of
   being forced square; squareBleedImage in include.js reads that
   attribute. aspect-ratio is explicitly cleared to auto here: with the
   element's height driven by top/bottom (below) and width set
   explicitly by that same script, a lingering CSS aspect-ratio doesn't
   sit quietly alongside those — it actively recomputes height from the
   now-explicit width, overriding the top/bottom span and undoing the
   full-height fill this rule exists to create. */
.image-left {
  position: relative;
  overflow: hidden;
}
.image-left .card-with-headshot > div {
  min-width: 0;
}
.image-left .card-with-headshot > .headshot-placeholder,
.image-left .card-with-headshot > .logo-placeholder,
.image-left .card-with-headshot > .image-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  aspect-ratio: auto;
  width: auto;
  height: auto;
  margin-bottom: 0;
}

/* Top-3 Standings/Rider Leaderboard cards — a #1/#2/#3 rank block bled
   flush to the card's left edge and the logo/headshot bled flush to its
   right edge, both spanning the card's full (content-driven) height.
   Same "bleed against the OUTER .round-complete, not just
   .card-with-headshot" technique as Winning Team/Top Rider above (see
   that rule's own comment), just applied on both sides here — reserved
   padding on both sides of .top3-card itself keeps the in-flow text
   from running under either bled element. The image's width, and so
   its reserved padding-right, is content-driven (unlike Winning Team's
   fixed-height case) — set at runtime by squareBleedImage in
   include.js rather than a safe static value here. */
.top3-card {
  position: relative;
  /* Rank block's own 32px width, plus a 16px gap before the card's
     content starts. */
  padding-left: 48px;
}
.top3-card .card-with-headshot > .logo-placeholder,
.top3-card .card-with-headshot > .headshot-placeholder {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: auto;
  height: auto;
}
.top3-rank {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  font-weight: 800;
}
.top3-rank-1 { background: var(--brand-orange); color: var(--brand-white); }
.top3-rank-2 { background: var(--brand-navy); color: var(--brand-white); }
.top3-rank-3 { background: var(--brand-brown); color: var(--white-500); }

/* Hero stat — the metric each leaderboard is actually ranked by (PTS),
   pulled out of the regular .ts text-line list and given the visual
   weight of a headline number instead, per the reference layout for
   this rework. */
.top3-stat {
  font-size: 32px;
  font-weight: 800;
  line-height: 1.1;
  margin: 0 0 4px;
  color: inherit;
}
.top3-card .name {
  font-size: 16px;
  font-weight: 700;
}

/* Horizontal directory row — Rider Directory: Headshot | Name | Team |
   Country; Team Directory: Logo | Name | Country — each field its own
   column (rather than name+team+country stacked/combined next to the
   headshot, as in .card-with-headshot). The text columns split the
   remaining width evenly. The headshot/logo itself bleeds flush
   against the row's own top/bottom/left edges (past .module's 16px
   padding) and stays square — same position:absolute + JS-measured-
   width technique as every other headshot/logo card (see
   squareBleedImage in include.js), just bled to the LEFT edge here to
   match this row's own image-then-columns order instead of the
   sitewide content-left/image-right pattern. padding-left (reserving
   room for the now-out-of-flow image) is set dynamically alongside the
   width, since the row's height — and so the square's width — is
   itself content-driven. */
.rider-row,
.team-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 16px;
  /* Same white fill as the Top-3 blocks (.round-complete), overriding
     the navy-tinted .cat-editorial background this row also carries. */
  background: var(--round-complete-bg);
}
.rider-row > *:not(.headshot-placeholder) {
  flex: 1;
  min-width: 0;
}
.team-row > *:not(.logo-placeholder) {
  flex: 1;
  min-width: 0;
}
.rider-row .headshot-placeholder,
.team-row .logo-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: auto;
  height: auto;
}
.headshot-placeholder,
.logo-placeholder {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border: 0.5px solid var(--border-strong);
  background: var(--surface-1);
}
/* Larger variant for a detail page's own Identity module, vs. the
   small size used in list/roster cards. */
.headshot-placeholder.lg,
.logo-placeholder.lg {
  width: 96px;
  height: 96px;
}

/* Event Detail's Teams grid (#event-teams) — one team's whole entry is
   a single flush 4-up row, Team | Rider | Rider | Rider, using the
   same .team-tile/.rider-tile components as everywhere else instead
   of a stacked header+list. 1 row per page-row below lg (each tile
   would get too narrow otherwise), 2 rows per page-row at lg+. */
.event-teams-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  margin-bottom: 12px;
}
@media (min-width: 1024px) {
  .event-teams-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
/* No gap within a team's own row — the team and its 3 riders read as
   one continuous unit, each tile's own solid border (not dashed, per
   feedback that dashed borders aren't brand-approved anywhere on the
   site) doing the separating between adjacent flush tiles instead of
   spacing. 2 columns below sm (Team+Rider / Rider+Rider), full 4
   across from sm up — narrower than that and a rider tile has no room
   left for its own identity bar. */
.event-team-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0;
}
@media (min-width: 640px) {
  .event-team-row {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Rider tile — candidate replacement for the Rider Directory's row
   layout above (.rider-row): a vertical photo-card (square image on
   top, identity bar below) rather than a horizontal name/team/country
   row. Sample-only for now (pages/rider-card-sample.html) — not yet
   wired into riders.html. Brand tokens throughout — --navy-500,
   --ab-white (via --brand-navy below), plus --brand-orange for the
   rank-1 badge and --brand-brown for rank-3/Top Rider (see
   .rider-tile-badge-1/-3 below). Border is solid, not
   dashed — dashed borders aren't brand-approved anywhere on the site
   (this one included; an earlier pass over that feedback briefly
   dropped this tile's border entirely instead of just de-dashing it,
   which was a misread — the border itself was never the problem).
   The TILE itself (not just the photo) is 1:1 — matches the original
   reference PNG, where the footer bar reads as part of one square card
   rather than sitting below it. aspect-ratio here is the reliable case
   (this element's WIDTH is fixed by its grid/flex track, height
   derives from that).
   NOT itself a link — it wraps two SEPARATE ones (.rider-tile-main and,
   where present, .rider-tile-stat) that go to different destinations
   (a rider's own profile vs. the leaderboard/results view the stat is
   pulled from), and an <a> can't nest inside another <a>. Still column
   flexbox: .rider-tile-main takes whatever height .rider-tile-stat's
   own fixed proportion (see its own comment) doesn't.
   container-type:inline-size turns this into a query container so
   every scaling value below (flag size, badge/name/stat font-sizes,
   footer padding/gap — anything in cqw units) resolves against THIS
   element's own rendered width, not the viewport. That's what makes
   the whole tile scale as a unit if the grid it sits in changes column
   count (e.g. Rider/Team Directory moving to 5 or 6 columns) — cqw
   values shrink or grow with the tile automatically, no breakpoint
   needed. Every cqw value below was hand-converted from this
   component's original fixed-px values at its 280px sample width (e.g.
   14px name / 280px = 5cqw), so nothing changes size at that width —
   only OTHER widths now scale correctly, where before they'd have held
   those px values fixed regardless of how small the tile got. */
.rider-tile {
  display: flex;
  flex-direction: column;
  aspect-ratio: 1 / 1;
  container-type: inline-size;
  border: 0.5px solid var(--border-strong);
  background: var(--ab-white);
  overflow: hidden;
}
/* Wraps the photo + identity bar, linking to the rider's own profile —
   everything .rider-tile itself used to be before .rider-tile-stat
   needed a separate destination. Internally the same column-flex split
   as before: .rider-tile-footer's fixed proportion, .rider-tile-photo
   absorbing the rest. */
.rider-tile-main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  text-decoration: none;
  color: inherit;
}
.rider-tile-main:hover .rider-tile-footer {
  background: var(--nav-outline-hover-bg);
  cursor: pointer;
}
/* Photo fills whatever height the footer (fixed proportion, below)
   doesn't take, bled full-width and to the tile's top/left/right edges
   — no aspect-ratio of its own anymore now that the OUTER tile is the
   square; background-size:cover crops to fit (never stretches), so a
   portrait headshot reads as cropped rather than distorted. Anchored to
   "center top", not "center center" — the top of the image must land on
   the top of the card with nothing trimmed off that edge; cover's crop
   comes off the bottom only (where the footer bar already interrupts
   the photo anyway). */
.rider-tile-photo {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  background-color: var(--navy-500);
  background-size: cover;
  background-position: center top;
}
/* Occasional multi-purpose corner tag (Just Signed, Top Rider, etc.) —
   most tiles carry none; add the span only when a rider actually has
   something to flag. Flush into the photo's own top-left corner
   (top:0/left:0, not inset) — reads as a corner-mounted tag rather than
   a floating chip — with generous internal padding so the label has
   real breathing room. Sand-on-navy-text rather than the sitewide
   .badge's cobalt, since this sits on a photo (needs real contrast, not
   a status-color convention) and sand is the one brand accent not
   already claimed by .page-title-bar (orange) or .cat-editorial (navy). */
.rider-tile-badge {
  position: absolute;
  top: 0;
  left: 0;
  padding: 2.86cqw;
  background: var(--brand-sand);
  color: var(--brand-navy);
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 5.71cqw;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
/* Rank-specific color set for a #1/#2/#3 leaderboard badge — same
   orange/navy/brown hues as .top3-rank-1/2/3 (Standings/Rider
   Leaderboard's own top-3 treatment) so a rank badge reads consistently
   whether it's this tile or that card. Not identical token-for-token,
   by explicit request: .top3-rank-1/2 both use --brand-white
   (--white-500) text, this uses --brand-navy for #1 and --ab-white
   (pure white, not the off-white --white-500) for #2 — #3 does match
   .top3-rank-3 exactly (--brand-brown/--white-500), shared by Top Rider
   too (rider-tile-badge-3 doubles as that role badge, per feedback).
   Add a -4/-5/etc. modifier here, following this same pattern, if a use
   case beyond the top 3 ever needs its own rank color. */
.rider-tile-badge-1 { background: var(--brand-orange); color: var(--brand-navy); }
.rider-tile-badge-2 { background: var(--brand-navy); color: var(--ab-white); }
.rider-tile-badge-3 { background: var(--brand-brown); color: var(--white-500); }
/* Identity bar — flag, name (+ team sub-line), and a "view profile"
   arrow chip. Same --ab-white as the outer tile (not .cat-editorial's
   navy-25 wash), so the sand badge above and the sand arrow chip here
   read as the tile's only color accent.
   Height is 15cqw, NOT flex:0 0 15% — a plain percentage here resolves
   against this element's own immediate flex parent, .rider-tile-main,
   and .rider-tile-main is NOT always the full square: when
   .rider-tile-stat is also present, .rider-tile-main only gets the
   85% .rider-tile-stat's own flex:0 0 15% (resolved correctly, since
   .rider-tile-stat IS a direct child of the square) leaves behind —
   making a percentage-based footer height only 12.75% of the actual
   square (15% of that 85%) instead of the intended 15%, visibly
   shorter than .rider-tile-stat whenever both are on the same card.
   cqw sidesteps this entirely: it's always relative to .rider-tile
   itself (the container-type:inline-size element), regardless of DOM
   nesting depth, and since the tile is square (aspect-ratio:1/1),
   15cqw (15% of the tile's WIDTH) equals 15% of its HEIGHT too — so
   this reliably matches .rider-tile-stat's own 15% whether or not that
   sibling is even present. No right padding: the arrow bleeds flush to
   the tile's own right edge (.rider-tile's overflow:hidden clips it
   there), same "own segment bleeds to the edge" idea as
   .breaking-bar-arrow. align-items:center — flag and the name/team
   group both vertically center in this row (a top-aligned variant was
   tried and reverted per feedback: the flag stays vertical-align
   middle, and the whole Rider Name/Team Name block centers as one
   unit, not pinned to the flag's top edge). */
.rider-tile-footer {
  display: flex;
  flex: 0 0 15cqw;
  align-items: center;
  gap: 3.57cqw;
  min-height: 0;
  padding-left: 4.29cqw;
  background: var(--ab-white);
}
/* height:auto + aspect-ratio (not a fixed px height) so the flag scales
   in BOTH dimensions as width scales with the tile — a fixed height
   alongside a cqw width would hold the flag's own proportions hostage
   to whatever height happened to be set at the 280px sample size. */
.rider-tile-flag {
  flex: 0 0 auto;
  align-self: center;
  display: block;
  width: 12.86cqw;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border: 0.5px solid var(--border-strong);
}
/* Name + team, stacked, centered as one block within the footer
   (align-self here matches the footer's own align-items:center — set
   explicitly so it stays correct even if that default ever changes)
   and deliberately NOT sized to grow the footer taller:
   .rider-tile-footer keeps its fixed 15%-of-square height regardless of
   this stack's own content height (min-height:0 above prevents flex's
   default intrinsic-min-height-wins-over-flex-basis behavior), so
   name+team together must fit inside that existing height, not the
   other way around. overflow:hidden here is the backstop if they ever
   don't. */
.rider-tile-name-group {
  flex: 1 1 auto;
  align-self: center;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* Standard single-line truncation — simpler and more predictable than
   the earlier overflow:visible + masking-strip approach (which
   guaranteed a gap before the arrow but relied on an extra DOM element
   and a hover-state color it had to track); ellipsis handles the
   "don't collide with the arrow" problem as a side effect of clipping
   at the box's own edge, no separate element needed. */
.rider-tile-name {
  flex: 0 0 auto;
  min-width: 0;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 5cqw;
  font-weight: 700;
  line-height: 1;
  color: var(--brand-navy);
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Team sub-line — smaller and unbolded (vs. .rider-tile-name's 5cqw/700)
   so it reads as secondary identity, not a second heading; --text-
   secondary (the sitewide muted-but-not-.ts-muted tone) for the same
   reason. Uppercase like the name above it (per feedback — reversed
   from an earlier sentence-case version), so the size/weight
   difference alone carries the hierarchy. font-size is 3.93cqw (11px
   @280px) after two rounds of +1px feedback (started at 3.21cqw/9px).
   line-height:1 on both this and .rider-tile-name, plus this element's
   own tightened margin-top (0.18cqw, half its original 0.36cqw), is
   the tighter line-spacing from that same feedback round. */
.rider-tile-team {
  flex: 0 0 auto;
  min-width: 0;
  margin-top: 0.18cqw;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 3.93cqw;
  font-weight: 400;
  line-height: 1;
  color: var(--text-secondary);
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* View-profile arrow chip — matches .breaking-bar-arrow-box's own
   treatment: a true 1:1 square spanning the bar's FULL height (flush
   top and bottom, not inset), same sand fill as .rider-tile-badge above
   so the tile's two accent marks read as one system. Now that the
   footer's own height is a percentage (previous comment on
   .rider-tile-footer) there's no fixed px value left to hardcode here,
   so align-self:stretch establishes a definite cross size for this flex
   item first (overriding the footer's own align-items:center just for
   this child), and aspect-ratio:1/1 derives the width from THAT — the
   standard reliable case for a stretched flex item, not the
   position:absolute/top-bottom-span case .rider-tile-photo's history
   warns about. */
.rider-tile-arrow {
  flex: 0 0 auto;
  align-self: stretch;
  aspect-ratio: 1 / 1;
  width: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--brand-sand);
}
/* Fills the chip completely — no extra width/height clamp, no
   translate nudge. The icon's own viewBox already carries built-in
   margin around the glyph (verified 6px on all sides at its native
   48px box — see .breaking-bar-arrow-box's own comment), so sizing the
   svg to 100% x 100% is enough breathing room on its own; stacking a
   smaller explicit size on top of that was double-padding it. */
.rider-tile-arrow svg {
  width: 100%;
  height: 100%;
  display: block;
}
.rider-tile-arrow svg path {
  fill: var(--brand-navy);
}
/* Optional third strip — for placements that pair a badge with a
   contextual stat (Top Rider + time, Leaderboard #1-3 + points, etc.);
   the Rider Directory's own use of this tile has neither and simply
   omits this element entirely, same "absent markup, not a hidden/empty
   state" approach as .rider-tile-badge above. A link in its own right —
   NOT nested in .rider-tile-main — since it goes to a different
   destination than the rider's own profile (the stat's source view:
   Rider Leaderboard for a points stat, an Event's Results for a time
   stat), and an <a> can't contain another <a>; see .rider-tile's own
   comment. flex:0 0 15% of .rider-tile (a sibling of .rider-tile-main,
   not of .rider-tile-footer — they're no longer in the same flex
   container now that .rider-tile-main exists), so .rider-tile-main's
   own flex:1 absorbs whatever this doesn't take. A plain top border
   (not the footer's own hover/arrow treatment) reads as a second,
   separate row under the identity bar; its own :hover below gives it
   independent rollover feedback now that it's a real link. Label/value
   colors and sizes here are a first pass — exact treatment per
   placement (Top Rider vs. Leaderboard) is still open.
   RULE: the label follows the href's destination, not the badge —
   anything linking to Rider Leaderboard/Team Standings is Points,
   anything linking to Event Results is Time. A "Top Rider"/"Winning
   Team" badge does NOT imply a Points stat; it's paired with whichever
   stat that specific placement's card actually links to. */
.rider-tile-stat {
  display: flex;
  flex: 0 0 15%;
  align-items: center;
  gap: 2.86cqw;
  min-height: 0;
  padding: 0 4.29cqw;
  background: var(--ab-white);
  border-top: 0.5px solid var(--border-strong);
  text-decoration: none;
  color: inherit;
}
.rider-tile-stat:hover {
  background: var(--nav-outline-hover-bg);
  cursor: pointer;
}
.rider-tile-stat-label {
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 5cqw;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--brand-navy);
}
/* Value only — scaled back down from an earlier 10.71cqw per feedback
   (too large relative to .rider-tile-stat-label, which stays at 5cqw
   unchanged). line-height:1 stays regardless of size — required so the
   line box never pushes taller than .rider-tile-stat's own unchanged
   (15%) height, growing or clipping the bar. */
.rider-tile-stat-value {
  margin-left: auto;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 7.14cqw;
  line-height: 1;
  font-weight: 700;
  color: var(--brand-navy);
}

/* Dual-stat variant of .rider-tile-stat — for placements that need TWO
   figures at once (Team Profile's own Roster: PTS + Rank together,
   neither being the card's own single "hero" stat the way Top
   Rider/Leaderboard's Time/PTS is). Same base (.rider-tile-stat is
   still applied alongside this modifier — height/padding/background/
   hover/link behavior all come from there unchanged). Each half reuses
   .rider-tile-stat-label/.rider-tile-stat-value UNCHANGED — same
   label-left/value-right-via-margin-left:auto layout the Leaderboard
   #1-3 cards use for their own single PTS stat, per feedback, just
   scoped to a half-width flex:1 group instead of the whole bar; cqw
   sizing means the exact same classes come out proportionally smaller
   automatically at this tile's narrower width (5-across vs. a
   standalone card), no separate size tuning needed. */
.rider-tile-stat-dual {
  gap: 3.57cqw;
}
.rider-tile-stat-group {
  display: flex;
  align-items: center;
  flex: 1 1 0;
  min-width: 0;
}
/* Center divider — sits exactly at the midpoint since both groups are
   equal flex:1 halves. Same vertical-rule treatment as team-bio-stats/
   team-roster-stats elsewhere on this site for this same PTS/Rank
   pairing. */
.rider-tile-stat-group + .rider-tile-stat-group {
  border-left: 0.5px solid var(--border-strong);
  padding-left: 3.57cqw;
}
/* Round-result context specifically (Winning Team/Runner-up/Top
   Rider's own dual-stat: Time + FLTS) — Time's value ("00:00.00") is
   much longer than Team Profile's own PTS/Rank figures this pattern
   was built for, and overflowed label-left/value-right within an
   equal half-width group at this tile's narrower size (2-up-paired
   contexts especially — confirmed overflowing at ~152px). Kept the
   label-left/value-right layout (no stacking) by giving the groups
   unequal widths instead — Time gets 3 parts, FLTS 2, since FLTS is
   always a short 1-2 digit count and Time is a full timestamp. Scoped
   here rather than changed on .rider-tile-stat-group itself so Team
   Profile's Roster (an equal PTS/Rank split, not nested in
   .round-result-tiles) is untouched. */
.round-result-tiles .rider-tile-stat-group:first-child {
  flex: 3 1 0;
}
.round-result-tiles .rider-tile-stat-group:last-child {
  flex: 2 1 0;
}

/* Team tile — the team-side twin of .rider-tile directly above, same
   structure/rules top to bottom, following this codebase's existing
   convention of parallel rider/team classes (.rider-row/.team-row,
   .headshot-placeholder/.logo-placeholder) rather than one shared base
   class. Sample-only for now (pages/team-card-sample.html) — not yet
   wired into teams.html. Reads from the new team-roster.js (PJL_TEAMS)
   + country-codes.js (PJL_COUNTRY_CODES), same as .rider-tile reads
   from rider-roster.js.
   NOT itself a link, for the same reason as .rider-tile: it wraps
   .team-tile-main (→ team profile) and, where present, .team-tile-stat
   (→ Team Standings/Event Results) as two separate anchors.
   container-type:inline-size — same reasoning as .rider-tile's own
   comment: every cqw value below scales with THIS tile's own rendered
   width, so the card scales as a unit if the grid's column count
   changes, rather than holding flag/text sizes fixed while only the
   photo/footer proportions (already %-based) respond. */
.team-tile {
  display: flex;
  flex-direction: column;
  aspect-ratio: 1 / 1;
  container-type: inline-size;
  border: 0.5px solid var(--border-strong);
  background: var(--ab-white);
  overflow: hidden;
}
.team-tile-main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  text-decoration: none;
  color: inherit;
}
.team-tile-main:hover .team-tile-footer {
  background: var(--nav-outline-hover-bg);
  cursor: pointer;
}
/* Unlike .rider-tile-photo, this does NOT crop (background-size:cover)
   — a logo mark can't lose part of itself the way a cover-cropped
   headshot's edges can; a team's mark needs to stay fully intact and
   legible. contain scales the whole logo down to fit instead, which
   means it won't always fill the square (a logo whose own aspect ratio
   isn't 1:1 leaves letterboxing on two sides) — background-color is
   what fills that gap, overridden per-team via inline style to that
   team's own color (team-roster.js's `color` field, sampled directly
   from each logo's own flat ground) rather than one shared fallback
   tint, so the letterboxing reads as "the logo's own background
   extended to the tile's edges," not a mismatched color. no-repeat is
   required alongside contain — background-repeat defaults to tiling
   the scaled-down image across the leftover space otherwise. */
.team-tile-photo {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  background-color: var(--navy-500);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}
.team-tile-badge {
  position: absolute;
  top: 0;
  left: 0;
  padding: 2.86cqw;
  background: var(--brand-sand);
  color: var(--brand-navy);
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 5.71cqw;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.team-tile-badge-1 { background: var(--brand-orange); color: var(--brand-navy); }
.team-tile-badge-2 { background: var(--brand-navy); color: var(--ab-white); }
.team-tile-badge-3 { background: var(--brand-brown); color: var(--white-500); }
/* .team-tile-badge/.rider-tile-badge (below) are both dual-purpose:
   a #1/#2/#3 rank (Directory/Leaderboard top-3, text-length-uniform by
   nature) or a Winner/Runner-up/Top Rider role label
   (renderRoundResultTiles(), include.js) — width left content-driven
   (padding only) so a rank badge never renders wider than "#1" needs.
   That same content-driven width means the three ROLE labels, whose
   text length actually varies ("Winner" vs "Runner-up" vs "Top
   Rider"), come out three different widths instead — per feedback,
   Winner/Runner-up/Top Rider must always render as the same-size
   pill, the same way .mini-row-badge already fixes its own width
   (76px) for this exact trio's compact-row version. .role-badge (only
   on the 3 role-label spans — include.js's own template strings,
   plus the sample pages' matching examples — never the plain rank
   ones) opts in to that same fixed-width treatment here, sized in
   cqw (not px) to keep scaling with the tile the same way this badge's
   own font-size/padding already do — 41cqw comfortably fits
   "Runner-up", the widest of the three at this font size, measured
   directly. */
.team-tile-badge.role-badge,
.rider-tile-badge.role-badge {
  width: 41cqw;
}
/* flex:0 0 15cqw, not 15% — see .rider-tile-footer's own comment for
   why a percentage here (resolving against the variable-height
   .team-tile-main instead of the true square) makes this bar shorter
   than .team-tile-stat whenever both are present on the same card. */
.team-tile-footer {
  display: flex;
  flex: 0 0 15cqw;
  align-items: center;
  gap: 3.57cqw;
  min-height: 0;
  padding-left: 4.29cqw;
  background: var(--ab-white);
}
.team-tile-flag {
  flex: 0 0 auto;
  display: block;
  width: 12.86cqw;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border: 0.5px solid var(--border-strong);
}
/* Same reasoning as .rider-tile-name's own comment. */
.team-tile-name {
  flex: 1 1 auto;
  min-width: 0;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 5cqw;
  font-weight: 700;
  color: var(--brand-navy);
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.team-tile-arrow {
  flex: 0 0 auto;
  align-self: stretch;
  aspect-ratio: 1 / 1;
  width: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--brand-sand);
}
.team-tile-arrow svg {
  width: 100%;
  height: 100%;
  display: block;
}
.team-tile-arrow svg path {
  fill: var(--brand-navy);
}
/* Same destination-driven label rule as .rider-tile-stat above: Points
   for a Team Standings link, Time for an Event Results link — never
   inferred from the badge ("Winning Team" pairs with Time, not
   Points). */
.team-tile-stat {
  display: flex;
  flex: 0 0 15%;
  align-items: center;
  gap: 2.86cqw;
  min-height: 0;
  padding: 0 4.29cqw;
  background: var(--ab-white);
  border-top: 0.5px solid var(--border-strong);
  text-decoration: none;
  color: inherit;
}
.team-tile-stat:hover {
  background: var(--nav-outline-hover-bg);
  cursor: pointer;
}
.team-tile-stat-label {
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 5cqw;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--brand-navy);
}
/* Same reduction as .rider-tile-stat-value's own comment — value scaled
   down, .team-tile-stat-label untouched. */
.team-tile-stat-value {
  margin-left: auto;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 7.14cqw;
  line-height: 1;
  font-weight: 700;
  color: var(--brand-navy);
}

/* Horse card — IA §5 Rider/Horse Boundary's own informational listing
   (rider-profile.html's Horses section currently shows just a bare
   name per horse; this is the fuller card that content is a stand-in
   for). Follows .rider-tile's own conventions closely (bold uppercase
   navy name; label/value stat chips with border-strong dividers), just
   with no photo — no horse photo asset exists in this project, and
   nothing here asked for one — so there's no bled-image row, only the
   stat-chip rows a rider-tile carries below its own photo. Two such
   rows, not one flat 7-field list: Identity (Owner/Breed) stacked
   under Name, then Measurements (Height/Weight/Jumping Scope/Canter
   Stride/Speed) below that. Plain .module card (border/padding/
   background come from there). Sample-only for now
   (pages/horse-card-sample.html), same convention as .rider-tile/
   .team-tile before they were wired in. */
/* Fill + rule colors match .rider-tile-stat/.rider-tile-footer exactly
   (plain white, --border-strong dividers) rather than the generic
   .cat-editorial category tint (navy-25 fill, navy-500 border) this
   card would otherwise inherit as a plain .module. */
.horse-tile {
  background: var(--surface-1);
  border-color: var(--border-strong);
  color: var(--text-primary);
}
/* Bled on all sides but the bottom (negative margin past .module's own
   12px padding, matching padding re-added so the text itself doesn't
   shift) so both the tinted fill and the rule below Name reach the
   card's true edges — top/left/right included — instead of just the
   content column inside the card's padding. Fill is --white-50 (there
   is no --white-25 in the palette — --white-50 is that family's
   lightest step, same token rider-profile.html's own Name/Country row
   already uses for this same "slight tint" purpose) so Name reads as
   its own band, distinct from the plain-white stat rows below it. */
.horse-tile-name {
  margin: -12px -12px 0;
  padding: 12px 12px 12px;
  background: var(--white-50);
  border-bottom: 0.5px solid var(--border-strong);
  font-size: 18px;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--brand-navy);
}
.horse-tile-stats {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  row-gap: 12px;
  margin-top: 12px;
}
/* Identity (Owner/Breed) and Measurements (Height/Weight/Jumping
   Scope/Canter Stride/Speed) are each their own .horse-tile-stats row
   — a divider between the two only, not around every row. */
.horse-tile-stats + .horse-tile-stats {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 0.5px solid var(--border-strong);
}
/* Each stat — label/value stacked, flex:1 1 0 so they share the row's
   width evenly at full size, wrapping at narrower widths (row-gap on
   .horse-tile-stats above covers the vertical gap once they do).
   Order differs row to row, set in the markup, not here: Measurements
   reads value-then-label (a big figure first, same convention as
   .rider-bio-stat-num/-label and .mini-row-stat); Identity reads
   label-then-value instead (small caption first, so Owner/Breed's
   often-longer values get the bigger, bottom line to themselves) —
   both share the same .horse-tile-stat-value/-label typography either
   way, and the child-adjacency rule below adds the same gap between
   them regardless of which element ends up on top. */
.horse-tile-stat {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-width: 84px;
  padding: 0 12px;
}
.horse-tile-stat > * + * {
  margin-top: 4px;
}
.horse-tile-stat + .horse-tile-stat {
  border-left: 0.5px solid var(--border-strong);
}
.horse-tile-stat:first-child {
  padding-left: 0;
}
.horse-tile-stat-value {
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  color: var(--brand-navy);
}
.horse-tile-stat-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
@media (max-width: 639.98px) {
  .horse-tile-stat {
    min-width: 40%;
  }
  .horse-tile-stat:nth-child(odd) {
    padding-left: 0;
    border-left: none;
  }
}
/* Height/Weight/Speed (3 stats, unlike every other .horse-tile-stats
   row's 2) shouldn't wrap into a lone 3rd item at mobile the way the
   40% min-width above assumes every row will (fine for 2 items, but
   3×40% guarantees a wrap) — scoped to a modifier class on just this
   row rather than changing the shared rule, since Owner/Breed and
   Jumping Scope/Canter Stride still want that normal 2-up mobile wrap.
   min-width:0 replaces the 84px desktop floor too, not just the 40%
   mobile one — flex-wrap:nowrap without it would force this row's own
   3 stats to overflow the card once their combined 84px floors (252px)
   exceed the card's available width, rather than letting them shrink
   to fit. The mobile nth-child(odd) border/padding removal above also
   assumed a new line starts at every odd item, which no longer happens
   here — reset for children 2 and 3 so the divider between every stat
   still shows, same as this row already renders above 640px. */
.horse-tile-stats-nowrap {
  flex-wrap: nowrap;
}
.horse-tile-stats-nowrap .horse-tile-stat {
  min-width: 0;
}
@media (max-width: 639.98px) {
  .horse-tile-stats-nowrap .horse-tile-stat:nth-child(odd) {
    padding-left: 12px;
    border-left: 0.5px solid var(--border-strong);
  }
  .horse-tile-stats-nowrap .horse-tile-stat:first-child {
    padding-left: 0;
    border-left: none;
  }
}

/* Event tile — links to an Event Detail page (Home, Tour Schedule,
   Results). No color reset — every usage is paired with
   .module-emphasized or a category class that already sets one; see
   the .rider-card/.team-card note above for why that matters here. */
.event-tile {
  display: block;
  text-decoration: none;
}
.event-tile:hover {
  cursor: pointer;
}
/* Event name (the .th inside an event card, e.g. "Event 03") — scoped to
   .event-tile rather than .th itself, since .th is also the generic
   name/title style used by non-event cards (Team Name, Rider Name, etc.)
   that shouldn't be affected. */
.event-tile .th {
  text-transform: uppercase;
}
/* Hover, one per state — the card's own plain grey border (0.5px
   var(--border-strong) at rest) switches to a solid version of that
   state's own color instead of a background tint, so the whole card
   outline (not just the permanent 4px left accent stripe) reads as
   state-colored on hover. No fill change — kept the resting white
   background instead of the former -25 tint wash. */
.event-tile.module-complete:hover {
  border-color: var(--complete-border);
}
.event-tile.module-emphasized:hover {
  border-color: var(--emphasized-border);
}
.event-tile.module-upcoming:hover {
  border-color: var(--upcoming-border);
}

/* Full-width schedule event card — 3-column layout (event identity |
   venue/location/date | action buttons). Scoped to Tour Calendar's own
   full schedule list for now — other event-card contexts (Home's
   carousel, the status-row summary cards above this same list, Results)
   keep their current layout until this redesign extends to them.
   Layered on top of the existing .module-complete/-next/-emphasized/
   -upcoming state classes (color/border only) rather than replacing
   them, so state coloring stays one shared system across every card
   format, present and future. */
.event-card {
  display: flex;
  align-items: flex-start;
  gap: 20px;
}
/* 107px = the width that lines up .event-card-details (venue/location/
   date) with where "Regular Season" starts in .leg-divider above these
   cards: the card's own 4px border-left + 12px padding + 107px this
   column + 20px .event-card's own gap = 143px, matching
   .leg-divider-name's 131px + .leg-divider-part's own 12px
   padding-left. */
.event-card-info {
  flex: 0 0 auto;
  width: 107px;
}
/* Narrower fixed info column in the events overview (Tour Calendar's
   status-row / Home's events-overview) — same reasoning as the
   carousel's own override below: this column is a fraction of a
   3-across grid, not the full page-width row .event-card-info's 160px
   was sized for, so the same fixed width left barely any room for
   .event-card-details and badly wrapped venue names. */
.status-col .event-card-info {
  width: 90px;
}
/* Previous/Next (every non-featured column — :not(:has(.module-
   emphasized)) excludes whichever column is currently Current) read as
   compact, list-like previews rather than full peer cards: smaller
   padding, narrower info column, smaller type — closer to the full
   schedule list's own density below, rather than each card contending
   equally with the featured one for attention. Higher specificity than
   .module-complete/-emphasized/-upcoming's own 12px padding and the
   .status-col .event-card-info rule just above, so this wins for
   these columns without touching the featured card or the full list. */
.status-col:not(:has(.module-emphasized)) .event-tile {
  padding: 8px;
}
.status-col:not(:has(.module-emphasized)) .event-card-info {
  width: 72px;
}
.status-col:not(:has(.module-emphasized)) .event-card-name,
.status-col:not(:has(.module-emphasized)) .event-card-venue,
.status-col:not(:has(.module-emphasized)) .event-card-location,
.status-col:not(:has(.module-emphasized)) .event-card-date {
  font-size: 12px;
}
/* Smaller/muted rather than removed — color alone isn't reliable enough
   (WCAG 1.4.1, and the pale state tints read close together for
   colorblind users), but it no longer needs equal visual weight with
   the event name once the border-left accent + this redesign's overall
   structure already carry most of the state signal. */
.event-card-state {
  margin: 0 0 4px;
  font-size: 11px;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  /* white-600 — one step lighter/lower-contrast than --text-muted
     (white-700), so this reads even more subdued against the card's
     white fill. Still an on-palette value, just reused directly rather
     than adding a duplicate token (already --border-strong elsewhere). */
  color: var(--border-strong);
}
/* Current's own label picks up the brand emphasis color instead of the
   subdued default above — the one state where this text should draw
   the eye, not recede. */
.module-emphasized .event-card-state {
  color: var(--brand-orange);
}
.event-card-name {
  margin: 0;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  color: inherit;
}
.event-card-details {
  flex: 1 1 auto;
  min-width: 0;
}
.event-card-venue {
  margin: 0 0 2px;
  font-weight: 700;
  color: inherit;
}
.event-card-location {
  margin: 0;
  color: var(--text-secondary);
}
.event-card-date {
  margin: 0;
  font-weight: 700;
  color: var(--text-secondary);
}
/* Right column — one button per applicable action, stacked; a card in
   the Current state can show all three at once (Watch Live + Tickets +
   Results), every other state shows at most one. */
.event-card-actions {
  flex: 0 0 auto;
  /* Fixed at 110px — fit-content alone only matches buttons within one
     card's own column (a lone "Tickets" card ends up narrower than the
     Watch Live card next to it), so this is set instead to the widest
     button anywhere in the list (Watch Live, ~109px rendered), with
     align-items:stretch making every other button match it — the same
     width all the way down the list, not just within each card. */
  width: 110px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}
/* Reuses .watch-now-btn as-is for its JS hook (the video-modal click
   handler already targets this class sitewide) — color still comes
   from .btn-orange, added alongside it in the HTML same as everywhere
   else it appears. width:auto (not a fixed fill) so .event-card-actions'
   own align-items:stretch is what sizes it, matching its siblings. */
.event-card-actions .watch-now-btn {
  position: static;
  display: block;
  width: auto;
  text-align: center;
  top: auto;
  right: auto;
}
/* Layout only — width:auto lets .event-card-actions' own
   align-items:stretch size this to match its siblings (the column's
   own width, fit to the widest button); color comes from whichever
   .btn-* is added alongside it in the HTML. */
.event-card-btn {
  display: block;
  width: auto;
  box-sizing: border-box;
  text-align: center;
}
/* Below sm, .event-card-actions' fixed 140px column (plus .event-card-info's
   own 160px) leaves too little room for .event-card-details to stay
   readable — buttons drop to their own full-width row below the info/
   details instead of squeezing a 3rd column. Row, not the desktop
   column stack, since a horizontal row of buttons uses the full-width
   space better than a tall vertical stack once it's not sharing width
   with anything else; each button grows to split that row evenly. */
@media (max-width: 639.98px) {
  .event-card {
    flex-wrap: wrap;
  }
  .event-card-actions {
    flex: 1 1 100%;
    width: auto;
    flex-direction: row;
  }
  .event-card-actions .watch-now-btn,
  .event-card-actions .event-card-btn {
    flex: 1 1 0;
  }
}
/* Same "buttons drop to a row below" treatment as the sm breakpoint
   above, applied unconditionally rather than by viewport width — a
   carousel item is narrow by its own nature (a scrolling card, not a
   full-width row), regardless of how wide the page itself is, so it
   needs the compact layout at every viewport. */
.carousel-item .event-card {
  flex-wrap: wrap;
}
.carousel-item .event-card-actions {
  flex: 1 1 100%;
  width: auto;
  flex-direction: row;
}
.carousel-item .event-card-actions .watch-now-btn,
.carousel-item .event-card-actions .event-card-btn {
  flex: 1 1 0;
}

/* "Strong need" back-links — reinstated via the two-tone
   .page-title-bar-back treatment (see that rule's own comment above),
   not this plain .back-link. Each of these is reached from genuinely
   multiple entry points and isn't itself a main-nav destination, so
   "back" has no single obvious meaning and no other nav element covers
   it (main-nav's own items land on directories/landing pages, not back
   to whichever specific list the user drilled in from):
     - Event Detail (event-complete.html, event-next-current.html,
       event-upcoming.html) — from Tour Calendar or Results
     - Event Full Results (event-results-complete.html,
       event-results-next-current.html) — from Results Landing or an
       Event Detail's "View Full Results" link
     - Rider Profile (rider-profile.html) — from Rider Directory, Rider
       Leaderboard, a Team's Roster, Results, News, or Video
     - Team Profile (team-profile.html) — from Team Directory, Team
       Standings, or Rider Leaderboard's Team column
   Each page's own ?from=/FROM-map script sets #back-link's href and
   #back-link-label's text — same param-driven pattern .back-link used
   before these were briefly removed pending this treatment (a same-
   color, pipe-separated fold into .page-title-bar was tried first and
   reverted as too crowded).
   News Item (news-item.html) also carried a (static, single-entry-
   point) back-link before that removal, but was NOT brought back here:
   it only ever has one real entry point (News Landing), so it never had
   the multi-entry-point ambiguity that justifies one. */

/* Contextual back-link — still used by the three external "destination"
   pages (tickets.html, pjl-store.html, fantasy.html), which carry NO
   other site chrome at all (no main-nav/secondary-nav/bottom-nav/
   footer) — this is their only way back to pjl.com, so it's the one
   case where the link is unconditionally required. */
.back-link {
  display: inline-block;
  font-size: 12px;
  color: var(--text-secondary);
  text-decoration: none;
  margin-bottom: 12px;
}
.back-link:hover {
  color: var(--text-primary);
  text-decoration: underline;
}
/* Rank number is always bold (.ranked-cell .rank-num, above) regardless
   of row — the rest of a top-3 row's cells stay plain weight like
   every other row, rather than reading as a bolded podium extension. */

/* Schedule leg divider — segments a schedule into legs (e.g. Americas /
   Europe / Middle East). Three fixed blocks: Leg Name | Season Part |
   Time of Year, each its own brand-color block.
   Explicit height (rather than letting padding + content drive it, like
   .leg-divider-part's own font used to) — matches .section-label-bar's
   own rendered height (33px) exactly, sidestepping a mismatch between
   its plain sans font and .section-label-bar's PJL Sans, which carries
   noticeably taller natural line-height at the same font-size (the same
   fixed-height-plus-align-items:center technique used sitewide for this
   exact class of problem — see .eyebrow-nav/.breaking-bar/.pjl-rail-compact).
   Sticky, stacked directly under .status-row (84px main-nav+.pjl-rail-
   compact offset, was 96px before that class replaced .page-title-bar
   sitewide on 2026-09-10, + that row's own 173px rendered height) AND
   stacked against
   each other — each of the 3 leg-dividers (.leg-divider-1/2/3 below)
   gets its own progressively larger `top`, one full leg-divider-height-
   plus-gap (33px + the row's own 12px margin = 45px) further down than
   the one before it. That's what makes them accumulate rather than each
   later one simply covering the previous at the same offset (the
   simpler alternative, and this row's own behavior relative to
   .status-row above it) — Americas stays put, Europe pins in just below
   it once reached, then Middle East below that, so all three remain
   visible as a growing trail while scrolling through the full schedule.
   The three segments already span the row's full width with solid
   brand fills, so unlike every other sticky bar on this site, the
   VISIBLE 33px bar itself needs no separate background to keep
   scrolling content from showing through it. The 12px gap below that
   bar is a different story though: once a second leg-divider stacks in
   below the first (see above), that gap sits between two sticky
   elements rather than in normal flow — as plain margin (outside the
   box, nothing painted there) it let scrolling event-card text show
   through in the stuck state (confirmed via elementFromPoint at that
   exact pixel — real content, transparent background). Pulled inside
   the box as padding-bottom instead, backed by this element's own
   --surface-0 fill (matching the page, so the gap still reads as a
   plain gap, just an opaque one now), fixes that without changing the
   stacking math at all — 33px bar + 12px padding is still the same 45px
   per-header footprint the .leg-divider-1/2/3 offsets below were
   already built on (these are a pre-JS fallback only, though — see
   tour.html's own script, which recomputes and overrides `top` inline
   from .status-row's real measured height and stuck position once it
   runs, since that height is content-driven and can genuinely differ
   from this single hardcoded measurement). z-index one below
   .status-row's own 28, so that row stays on top of this whole stack. */
.leg-divider {
  position: sticky;
  z-index: 27;
  height: 45px;
  background: var(--surface-0);
  display: flex;
  /* stretch (the flex default), not center — each .leg-divider-part's
     own colored fill needs to reach this row's full 33px height, not
     just be a shorter, vertically-centered block floating inside it
     with page background showing above/below. .section-label-bar fills
     its own box solid edge-to-edge; this needs to match that, not just
     match its OUTER box height while rendering a visibly shorter
     colored bar inside it. */
  align-items: stretch;
  padding-bottom: 12px;
}
.leg-divider-1 { top: 265px; }
.leg-divider-2 { top: 310px; }
.leg-divider-3 { top: 355px; }
.leg-divider-part {
  /* Its own flex row (rather than relying on padding + line-height for
     vertical position) so its text still centers correctly now that the
     part itself stretches to the full 33px row height above, taller
     than its own text+padding would naturally need. */
  display: flex;
  align-items: center;
  padding: 4px 12px;
  font-size: 15px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
}
/* Left end cap — was sized to its own label (so Americas/Europe/Middle
   East each rendered a different width); fixed to the widest label's
   own natural width ("Middle East", 130.625px, rounded up) instead, so
   all three legs' name blocks render the same width regardless of which
   region is shorter. Shorter labels (Europe) still hug the left edge,
   just with more sand fill trailing after them now. */
.leg-divider-name {
  flex: 0 0 131px;
  /* justify-content, not text-align — .leg-divider-part is a flex
     container now (see its own comment), so text-align no longer
     positions its content; flex-start is the default but stated
     explicitly here since it's no longer a no-op the way it looked
     before that change. */
  justify-content: flex-start;
  background: var(--navy-500);
  color: var(--white-500);
}
/* Middle block: grows to fill the remaining width, text left-justified */
.leg-divider-season {
  flex: 1 1 auto;
  justify-content: flex-start;
  background: var(--sand-500);
  color: var(--navy-500);
}
/* Right end cap — same fixed-width treatment as .leg-divider-name,
   sized to the widest time-of-year label ("Summer", 93.375px, rounded
   up) so Spring/Summer/Autumn's navy blocks all render the same width. */
.leg-divider-time {
  flex: 0 0 94px;
  /* flex-end, not text-align:right — see .leg-divider-name's comment;
     the old text-align:right stopped doing anything once its parent
     became a flex container, silently left-aligning Spring/Autumn
     (narrower than this box's own Summer-sized width) instead of
     hugging the right edge like before. */
  justify-content: flex-end;
  background: var(--navy-500);
  color: var(--white-500);
}

/* Scroll box — constrains a long stacked list (e.g. a schedule of leg
   dividers + event cards) to a fixed viewport-relative height, scrolling
   vertically only; nothing inside should ever cause horizontal scroll. */
.scroll-box {
  max-height: 70vh;
  overflow-y: auto;
  overflow-x: hidden;
}

/* Contextual tab nav — sub-navigation for a main-nav item with multiple
   child pages. Each section's tab set is its own component; the script
   in each highlights whichever tab matches the current page.
   Responsive: at narrow widths, tabs used to wrap onto extra lines,
   breaking the underline row. Instead the row scrolls horizontally
   (same swipeable pattern as .carousel-track).
   The underline is a full-bleed fill (stretching to the true viewport
   width, like .page-title-bar above), but the tab items stay at their
   current (content-bounds) width — unlike .page-title-bar, this row's
   own content genuinely needs to stay put (it's a scroll container:
   widening its available space would change how many tabs fit before
   scrolling kicks in). A first version tried this via a ::after
   pseudo-element instead of .tab-nav's own border-bottom, sized
   independently of the row's own box — but .tab-nav's own
   overflow-x:auto (needed for the scroll behavior) clips ANY
   descendant that paints outside its box, pseudo-elements included, so
   that line silently never reached past the row's own content width
   despite computing the right size. Fixed the same way
   .page-title-bar breaks out: .tab-nav's own box goes full-bleed
   (width:100vw + the calc(50% - 50vw) breakout), with padding-left/
   right reapplying --content-bounds so the actual tab items still sit
   exactly where they did before — only the border-bottom, now the
   row's own (unclipped, since it's the box's own edge, not a
   descendant), actually reaches the true viewport edges. */
.tab-nav {
  /* Sticky, pinned directly under .pjl-rail-compact (also sticky) —
     84px is .main-nav's 64px plus .pjl-rail-compact's own 20px (was
     96px total, main-nav plus .page-title-bar's own 32px, before that
     class was replaced sitewide by .pjl-rail-compact on 2026-09-10),
     so all three sit flush with no gap while stacked at the top.
     z-index one below .pjl-rail-compact's own 29. An explicit
     background is needed now that content scrolls underneath it (this
     row is otherwise transparent, sitting directly on .page's own
     off-white fill). */
  position: sticky;
  top: 84px;
  z-index: 28;
  background: var(--surface-0);
  display: flex;
  flex-wrap: nowrap;
  gap: 4px;
  overflow-x: auto;
  scrollbar-width: none;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-bottom: 16px;
  padding-left: var(--content-edge);
  padding-right: var(--content-edge);
  border-bottom: 0.5px solid var(--border-strong);
}
/* No margin/padding compensation needed here anymore — .page-title's
   own 16px trailing space is PADDING now (filled, part of its own box;
   see its own comment, styles.css), not an external margin this row
   ever needed to cancel-then-relocate. This row just starts flush
   against it.
   top:155.6px overrides .tab-nav's own plain 84px — same reasoning
   and same measured values as .page-title + .filter-row's identical
   override (styles.css, see its own comment for how 164.3/146.7 were
   derived). */
.page-title + .tab-nav {
  top: 155.6px;
}
@media (max-width: 639.98px) {
  .page-title + .tab-nav {
    top: 151.2px;
  }
}
.tab-nav::-webkit-scrollbar {
  display: none;
}

/* Parent-child back bar — Rider Profile/Team Profile each have exactly
   one true IA parent (Rider Directory/Team Directory), unlike Event
   Detail/Event Full Results (multiple genuinely different entry
   points: Tour Calendar vs. Results, an Event Detail's own "View Full
   Results" link, etc.), so this is a plain, FIXED link back to that one
   parent — not a referral chip whose label/target changed per entry
   point (the removed .page-title-bar-back treatment, and the ?from=
   param plumbing every linking page used to carry for it, both taken
   out sitewide). Most sports sites (nba.com, for one) don't bother
   with a "back to where you came from" affordance for a genuine
   cross-area jump (Standings straight to a Team page) — this exists
   only for the narrower, unambiguous parent/child case, where main-nav
   alone can't say "back" for you.
   Sits directly below .page-title (Rider/Team Profile's own visible
   heading, "Rider Detail"/"Team Detail" — .page-title-bar itself is
   empty, same sitewide rollout as every other page) in its own thin
   strip instead of living inside the title bar as a competing two-tone
   block; see the matching .page-title + .parent-back-bar rule further
   down (same gap-absorption pattern as .tab-nav/.filter-row/
   .status-row, styles.css), which cancels .page-title's own
   margin-bottom via collapsing so the two sit flush. margin-bottom:0
   of its own — the actual page content directly below (Rider/Team
   Profile's own Identity card, .rider-bio-fill/.team-bio-fill) wants
   to sit flush against this bar with no gap, per that card's own
   comment.
   Full-bleed, same width:100vw breakout technique as .tab-nav just
   above, with padding-left/right:var(--content-edge) keeping the
   actual link at the normal content inset.
   NOT sticky, for now — it was (top:87.5px, same slot .tab-nav uses),
   back when it sat directly under the sticky .page-title-bar. Now that
   .page-title (non-sticky, real rendered height that differs by
   breakpoint — 32px/56px font, so no single fixed top offset covers
   both) sits between them, a sticky top:87.5px here sticks EARLY and
   overlaps the still-visible title text while scrolling. Dropped
   sticky entirely rather than ship that overlap; revisit once
   .page-title's own sticky top/z-index are settled (its own comment)
   — this bar can then stack right below it using whatever technique
   that ends up needing. */
.parent-back-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-bottom: 0;
  padding-left: var(--content-edge);
  padding-right: var(--content-edge);
  background: var(--white-50);
  border-bottom: 0.5px solid var(--border-strong);
  color: var(--brand-navy);
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
}
/* Text and arrow both flip to orange-500 on hover — the arrow's own
   stroke="currentColor" (markup) picks this up for free, no separate
   svg rule needed. */
.parent-back-bar:hover {
  color: var(--brand-orange);
}
.parent-back-bar svg {
  flex: 0 0 auto;
}
/* Cancels .page-title's own 16px margin-bottom via collapsing —
   scoped to this pairing rather than a bare margin-top on
   .parent-back-bar itself, same reasoning as .tab-nav's own rule. */
.page-title + .parent-back-bar {
  margin-top: -16px;
}
.tab-item {
  flex: 0 0 auto;
  padding: 8px 16px;
  font-size: 12px;
  color: var(--text-secondary);
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  margin-bottom: -0.5px;
}
.tab-item:hover { color: var(--text-primary); }
.tab-item.active {
  color: var(--text-primary);
  font-weight: 700;
  border-bottom-color: var(--nav-solid-bg);
}

/* Dark variant — ported from Web 1.1 (2026-09-10). Style name:
   .tab-nav-dark (paired with .tab-nav on the same element, e.g.
   class="tab-nav tab-nav-dark"), for a tabbed page whose whole header
   zone (rail + title + tabs) is dark — pairs with .page-title-dark the
   same way the light .tab-nav pairs with .page-title's own light
   default. Reuses --nav-solid-* tokens (.main-nav's own navy-
   background text-color family) rather than inventing new ones —
   .tab-item's inactive/hover/active states just need the same
   light-on-navy treatment .main-nav's own items already use. Active
   state uses --brand-orange for the underline (not --nav-solid-text,
   which .tab-item.active already uses for its TEXT) — orange is this
   site's one consistent "primary emphasis" accent, so the active tab's
   own indicator reads as that accent rather than a plain light/dark
   inversion. */
.tab-nav.tab-nav-dark {
  background: var(--brand-navy);
  border-bottom-color: var(--nav-solid-divider);
}
.tab-nav-dark .tab-item {
  color: var(--nav-solid-text-muted);
}
.tab-nav-dark .tab-item:hover {
  color: var(--nav-solid-text);
}
.tab-nav-dark .tab-item.active {
  color: var(--nav-solid-text);
  border-bottom-color: var(--brand-orange);
}

/* Destination/storefront pages (Tickets, PJL Store, Fantasy) — reached
   from PJL.com but NOT themselves pjl.com pages (a real ticketing
   partner, e-commerce platform, or fantasy provider would sit here).
   Deliberately does not reuse .main-nav/.eyebrow-nav/bottom-nav/footer —
   just a minimal branded header, not the site's own navigation. */
.destination-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  background: var(--nav-solid-bg);
  margin-bottom: 16px;
}
.destination-header img {
  height: 40px;
  width: auto;
  display: block;
}
.destination-header .destination-name {
  font-size: 16px;
  font-weight: 700;
  color: var(--nav-solid-text);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border-left: 1px solid var(--nav-solid-divider);
  padding-left: 12px;
}

/* Generic placeholder swatch for a product/venue image */
.image-placeholder {
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 0.5px solid var(--border-strong);
  background: var(--surface-1);
  margin-bottom: 8px;
}

/* News lead story — 3-column grid, the image taking 1 column and its
   content (badge, headline, teaser, preview copy, date) taking the
   other 2 — same grid shape as .video-hero just below. The image's own
   aspect-ratio:4/3 (from .image-placeholder) produces its height
   natively from that column's width, no JS involved; grid's own
   default row-sizing (tallest item wins) then grows the block to match
   whichever of the two — image or content — ends up taller, rather
   than the image being made to match a content-driven height. Stacks
   to a single column on narrow viewports, same as .video-hero.

   The image then bleeds flush to the card's own top/bottom/left edges
   (past .module's 16px padding on those 3 sides — not the right side,
   which borders the content column, not the card's own edge) via
   negative margins rather than position:absolute: staying a normal,
   in-flow grid item is what lets it keep contributing to the row's
   auto-sizing (the "grows to fit the taller of image/content" behavior
   above needs that); pulling it out of flow to bleed, as tried
   earlier, stops it from sizing the row at all. width compensates for
   the 16px the negative left margin gives back, so the image's right
   edge — the one bordering the gap into content — lands exactly where
   the column's own edge would without any bleed. */
.news-card:has(.hero-story) {
  position: relative;
  overflow: hidden;
}
.hero-story {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}
.hero-story-image {
  grid-column: span 1;
  margin: -16px 0 -16px -16px;
  width: calc(100% + 16px);
}
.hero-story > div:last-child {
  grid-column: span 1;
}
@media (max-width: 639.98px) {
  .hero-story {
    grid-template-columns: 1fr;
  }
  /* The base rule's bleed (see .hero-story-image above) only ever
     compensates for the LEFT margin — correct at the side-by-side
     width, where the image's right edge is meant to stay put at the
     boundary facing the content column, not bleed into it. Once
     stacked, content moves below instead of beside it, so that same
     right edge is just an arbitrary stop 16px short of the card's true
     edge with nothing there to reserve room for — bleed it flush too,
     matching every other (always-stacked) video/news card. Bottom stays
     at 0 (not bled) so the grid's own 16px row-gap still opens up
     between the image and the body copy below it — bleeding all 4
     sides here would cancel that gap outright, leaving the body copy
     flush against the photo with no margin. */
  .hero-story-image {
    margin: -16px -16px 0 -16px;
    width: calc(100% + 32px);
  }
}

/* Alternative featured treatment — a prototype, not yet rolled out to
   every featured item (see conversation): the photo fills the entire
   card as its own background rather than sitting in its own column,
   with badge/headline/teaser/date overlaid directly on top of it
   instead of beside it. Opt-in via .hero-story-overlay alongside
   .hero-story/.card-with-headshot on the same element — no separate
   .hero-story-image child needed here, the markup sets the photo as
   this element's own background-image inline instead. min-height:40vh
   (rather than a content-driven height like .hero-story's own column
   layout, or this rule's own earlier fixed-height/aspect-ratio
   attempts, both of which clipped the badge/headline once the body
   copy grew past what 40vh could hold) gives this a predictable
   banner height that still grows taller on its own when a headline/
   copy combination needs more room than that. flex/space-between
   (rather than the earlier position:absolute/bottom:0 attempt, which
   anchored everything to the bottom as one block and also couldn't
   let it grow the card at all — an absolutely-positioned child never
   contributes to its parent's own height) is what lets the badge pin
   to the top edge while the headline/copy group pins to the bottom,
   as two separate flex children, and still lets a NORMAL-FLOW child
   push the card taller past min-height when there isn't enough spare
   room. Bleeds flush to the card's own edges the same way .hero-story-image
   does (negative margin, width compensation) since .module's 16px
   padding would otherwise leave a border around the photo. */
.hero-story-overlay {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: relative;
  min-height: 40vh;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  margin: -16px;
  width: calc(100% + 32px);
}
/* Scrim rather than a flat tint — legible text needs real contrast
   right behind it (bottom, where the copy sits) without darkening the
   whole photo, so the gradient fades out toward the top where nothing
   overlays it. */
.hero-story-overlay::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(2, 22, 42, 0.85) 0%, rgba(2, 22, 42, 0.35) 45%, rgba(2, 22, 42, 0) 75%);
}
.hero-story-overlay > div {
  position: relative;
  z-index: 1;
  padding: 24px;
}
/* Badge is this element's own direct flex child now (see markup and
   .hero-story-overlay's own comment above) rather than nested inside
   the padded headline/copy div, so it needs its own matching inset —
   margin (a flex item, not the div's padding box) rather than
   duplicating .hero-story-overlay > div's own 24px padding rule. */
.hero-story-overlay > .badge {
  position: relative;
  z-index: 1;
  margin: 24px 0 0 24px;
}
/* Title sized/styled/colored to match news-item-hero.html's own
   .article-hero-headline exactly (72px/50px mobile, PJL Sans 700,
   uppercase, pure white) per feedback — this overlay treatment and
   that article page's own hero overlay are now the same visual
   language, not just visually similar. margin-bottom explicit here —
   .hero-story .th's own 8px-0-4px (styles.css, "two canonical styles"
   consolidation) would otherwise still apply (same specificity, this
   rule just never touched margin before), but 4px reads as no gap at
   all against a headline this size; overriding it is what puts real
   space between the headline and the teaser below it, per feedback. */
.hero-story-overlay .th {
  max-width: 60%;
  margin-bottom: 16px;
  font-family: "PJL Sans", -apple-system, sans-serif;
  font-size: 72px;
  line-height: 0.9;
  text-transform: uppercase;
  color: var(--ab-white);
}
/* Sized up from the sitewide 14px .ts default per feedback — matches
   news-item-hero.html's own enlarged article body copy (16px/1.1,
   that page's own <style> block), same reference this overlay's title
   already borrows its own sizing from. */
.hero-story-overlay .ts {
  font-size: 16px;
  line-height: 1.1;
  color: var(--white-500);
}
@media (max-width: 1023.98px) {
  .hero-story-overlay .th {
    max-width: 80%;
  }
}
@media (max-width: 639.98px) {
  .hero-story-overlay .th {
    max-width: 100%;
    font-size: 50px;
  }
}
/* !important + placed after .news-card .ts-muted's own !important
   override (styles.css, ~line 3117) — same specificity, so source
   order decides; this one needs to win here since white/translucent
   reads correctly on a dark scrim where the sitewide muted-grey
   treatment that rule forces everywhere else would not. */
.hero-story-overlay .ts-muted {
  color: rgba(255, 255, 255, 0.7) !important;
}
/* Sand-on-navy rather than the sitewide .badge's cobalt-outline —
   same reasoning as .rider-tile-badge's own comment (styles.css):
   sitting directly on a photo needs real contrast, not a status-color
   convention that assumes a plain background behind it. */
.hero-story-overlay .badge {
  background: var(--brand-sand);
  border-color: var(--brand-sand);
  color: var(--brand-navy);
}

/* Video lead story — 2-column grid, the video grab and its content
   (badge, title, description, date) each taking one equal column, per
   feedback (was a 3-column 1/2 split, .video-hero's own grab narrower
   than its content). Stacks to a single column on narrow viewports,
   same as .hero-story above.

   Grab now bleeds full to the column's own top/left/bottom edges (past
   .module's 12px padding on those 3 sides, via negative margin/width
   compensation — same technique .hero-story-image uses, just -12px to
   match .video-card's own actual padding rather than that other
   component's 16px), per feedback — .video-hero used to leave every
   VOD grab, this one included, at its sitewide default (unbled) sizing;
   that's reversed now. aspect-ratio:16/9 (.video-thumb .image-
   placeholder's own base rule, styles.css) is untouched, so the grab
   keeps its proportions rather than stretching to fill the column —
   grid's own default row-sizing (tallest item wins) grows the block to
   match whichever of the two — image or content — ends up taller. */
.video-hero {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}
.video-hero > .video-thumb {
  margin: -12px 0 -12px -12px;
  width: calc(100% + 12px);
}
.video-hero > div:last-child {
  grid-column: span 1;
}
@media (max-width: 639.98px) {
  .video-hero {
    grid-template-columns: 1fr;
  }
  /* Bleed above only ever compensates for the LEFT margin — correct at
     the side-by-side width, where the grab's right edge is meant to
     stay put at the boundary facing the content column, not bleed into
     it. Once stacked, content moves below instead of beside it, so
     that same right edge is just an arbitrary stop 12px short of the
     card's true edge with nothing there to reserve room for — bleed it
     flush too, matching .hero-story-image's own identical mobile
     override. */
  .video-hero > .video-thumb {
    margin: -12px -12px 0 -12px;
    width: calc(100% + 24px);
  }
}

/* Featured videos — overlay treatment mirroring news.html's own
   .hero-story-overlay (badge/title/description overlaid directly on
   the grab, rather than beside it in its own column), per feedback —
   applied to TWO videos side by side in a .grid-2 row here instead of
   .hero-story-overlay's single full-width card, so aspect-ratio:16/9
   drives the height (matching every other video grab's own
   proportions, .video-thumb .image-placeholder's own base rule) rather
   than that rule's own min-height:40vh (tailored to one full-width
   photo, not two side-by-side ones). Distinct class from .video-hero
   (still used by Home's own single Featured Video module, unaffected)
   — this is video.html's own dedicated variant. Layered directly onto
   .video-thumb itself (not a separate child, unlike .hero-story-
   overlay's own markup) so the play-icon glyph (.video-thumb::after)
   and .duration-badge still work completely unmodified — both are
   already position:absolute against .video-thumb's own position:
   relative, which this class doesn't touch. Bleeds full on all 4
   sides now (past .module's 12px padding) rather than the plain
   .video-hero's own 3-sided bleed, since text overlays the grab now
   instead of sitting beside it — nothing left to reserve room for on
   the 4th side. */
.video-hero-overlay {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* Explicit — a column flex container defaults to align-items:stretch,
     which (with no explicit width set) stretches the badge and content
     div to the row's full cross-axis width. news.html's own
     .hero-story-overlay happens to dodge this only because that
     element also still carries the old .hero-story class, whose own
     align-items:start rule (styles.css) wins the cascade tie — not a
     deliberate fix, just a side effect of that markup choice. Set here
     directly rather than relying on the same kind of accidental
     inheritance, since .video-hero-overlay has no equivalent sibling
     class to borrow it from. */
  align-items: flex-start;
  position: relative;
  aspect-ratio: 16 / 9;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  margin: -12px;
  width: calc(100% + 24px);
}
.video-hero-overlay::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(2, 22, 42, 0.85) 0%, rgba(2, 22, 42, 0.35) 45%, rgba(2, 22, 42, 0) 75%);
}
.video-hero-overlay > .badge {
  position: relative;
  z-index: 1;
  margin: 12px 0 0 12px;
  background: var(--brand-sand);
  border-color: var(--brand-sand);
  color: var(--brand-navy);
}
.video-hero-overlay > div {
  position: relative;
  z-index: 1;
  padding: 12px;
}
/* No .ts rule here — per feedback, videos sitewide show title + date
   only, never a description, so this overlay never has one to style. */
.video-hero-overlay .th {
  color: var(--ab-white);
}
.video-hero-overlay .ts-muted {
  color: rgba(255, 255, 255, 0.7) !important;
}

/* News Item article body — the hero image floats beside the opening
   copy so paragraph text wraps around it, then continues at full width
   once the text runs longer than the image (a real article layout,
   unlike .hero-story's flex row above, where image and text stay
   parallel columns for the whole block regardless of text length).
   overflow: hidden establishes a block formatting context so this
   container's own height includes the floated image, not just the
   wrapped text. */
.article-body {
  overflow: hidden;
}
.article-image {
  float: left;
  width: 320px;
  margin: 0 16px 12px 0;
}
@media (max-width: 639.98px) {
  .article-image {
    float: none;
    width: 100%;
    max-width: 400px;
  }
}

/* Brand banner — ported from Web 1.1 (2026-09-10). Full-bleed
   solid-color statement module. Colors are NOT baked into the base
   class — pair it with exactly one of the 3 named variants below
   (fill, then text color, in that order, matching how they were
   specified):
     .brand-banner-white-navy   — white-500 fill, navy-500 text
     .brand-banner-navy-white   — navy-500 fill, white-500 text
     .brand-banner-navy-orange  — navy-500 fill, orange-500 text
   margin-top/bottom:0 is built into the base class itself — "no
   spacing top or bottom wherever it is placed" is this module's own
   always-on property, not a per-page override. */
.brand-banner {
  width: 100vw;
  margin-top: 0;
  margin-bottom: 0;
  margin-left: calc(50% - 50vw);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  padding: 64px 24px;
}
.brand-banner-white-navy {
  background: var(--white-500);
  color: var(--brand-navy);
}
.brand-banner-navy-white {
  background: var(--brand-navy);
  color: var(--white-500);
}
.brand-banner-navy-orange {
  background: var(--brand-navy);
  color: var(--brand-orange);
}
/* Text — ported from Web 1.1 (2026-09-10), same font treatment as that
   project's .news-hero-text (PJL Display italic, 0.9 line-height),
   width capped at 90% of --content-bounds (not this element's own
   full-bleed .brand-banner container, which would let lines run
   uncomfortably wide). color is deliberately NOT set here — inherits
   from whichever .brand-banner-* variant class is on the parent, so
   this one class works with all 3 variants without its own color
   logic. */
.brand-banner-text {
  margin: 0;
  max-width: calc(var(--content-bounds) * 0.9);
  font-family: "PJL Display", -apple-system, sans-serif;
  font-style: italic;
  font-weight: 700;
  font-size: 80px;
  line-height: 0.9;
  text-align: center;
}
@media (max-width: 639.98px) {
  .brand-banner-text {
    font-size: 72px;
  }
}
