/* Self-hosted, latin-subset variable font (weights 300-700 via the wght
   axis, one file) — the exact same file Google Fonts serves for this
   family/subset, just fetched from our own origin instead of
   fonts.googleapis.com + fonts.gstatic.com. No italic: the site never
   requested an italic weight from Google either, so `font-style: italic`
   rules (e.g. .empty-message) were already relying on the browser's faux
   (skewed) italic — unchanged here. */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('/static/fonts/roboto.woff2') format('woff2');
}

@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/roboto.woff2') format('woff2');
}

@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/roboto.woff2') format('woff2');
}

@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/static/fonts/roboto.woff2') format('woff2');
}

/* Reserves the scrollbar's own width permanently, whether or not a
   scrollbar is actually painted right now — so opening the game dialog
   (which sets `overflow: hidden` on html/body to stop the background
   scrolling under it — see .game-dialog-open below) never changes the
   page's content width. Without this, removing the scrollbar widens the
   viewport by its track's width and every fixed-width row shifts right
   for as long as the dialog is open. */
html {
  scrollbar-gutter: stable;
}

body {
  font-family: 'Roboto', system-ui, sans-serif;
  margin: 0;
  padding: 0;
  background: #e4e6eb;
  -webkit-user-select: none;
  user-select: none;
}

input,
textarea {
  -webkit-user-select: text;
  user-select: text;
}

a {
  color: #b71c1c;
}

/* Matches the original MUI theme's Typography scale (h1/h2 used for page
   and section titles, h3 for card/subsection titles) so headings read at
   the same size/weight as the old UI instead of the browser's UA defaults.
   The scale is the desktop one and only applies from 740px up (see below);
   on a phone the 45px h1 ate a third of the screen and wrapped every
   longer title over two or three lines, so the base sizes here are a step
   down. h3 is small enough already and is the same at every width. */
h1 {
  font-size: 2rem; /* 32px */
  line-height: 2.25rem; /* 36px */
  font-weight: 400;
  margin: 0 0 1rem;
}

h2 {
  font-size: 1.625rem; /* 26px */
  font-weight: 400;
  margin: 1rem 0;
}

h3 {
  font-size: 1.25rem; /* 20px, matches the original's card/subsection titles */
  font-weight: 500;
  margin: 0 0 1rem;
}

.team-subtitle {
  /* The original renders this one as its own lighter, larger variant
     instead of the generic h3 used elsewhere (contact-card titles, etc). */
  font-size: 1.5rem; /* 24px */
  font-weight: 300;
}

@media (min-width: 740px) {
  h1 {
    font-size: 2.8125rem; /* 45px */
    line-height: 2.8125rem; /* 45px */
  }

  h2 {
    font-size: 2.187rem; /* 35px */
  }

  .team-subtitle {
    font-size: 2.1875rem; /* 35px */
  }
}

hr {
  border: none;
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  margin: 0.5rem 0;
}

.text-center {
  text-align: center;
}

.sponsor-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
  margin: 1.5rem 0;
}

.sponsor-card {
  display: flex;
  width: 13rem;
  height: 10rem;
  align-items: center;
  justify-content: center;
}

.sponsor-card img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Self-hosted, subsetted to the ~40 glyphs actually referenced via the
   `icon`/`iconFilled` template funcs (see internal/httpserver/icons.go) —
   the full Google-hosted fonts are ~2000 glyphs each and add a cross-origin
   round trip on top. font-display: block avoids a flash of the fallback
   font before these tiny files (a few KB) finish loading. */
@font-face {
  font-family: 'Material Icons Outlined';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('/static/fonts/material-icons-outlined.woff2') format('woff2');
}

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('/static/fonts/material-icons-filled.woff2') format('woff2');
}

/* One glyph (the card, see the ticker) — a few hundred bytes. */
@font-face {
  font-family: 'Material Icons Round';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('/static/fonts/material-icons-round.woff2') format('woff2');
}

.material-icons,
.material-icons-outlined {
  font-family: 'Material Icons Outlined';
  font-weight: normal;
  font-style: normal;
  font-size: 24px;
  vertical-align: middle;
  line-height: 1;
}

.material-icons-filled {
  font-family: 'Material Icons';
  font-weight: normal;
  font-style: normal;
  font-size: 24px;
  vertical-align: middle;
  line-height: 1;
}

.material-icons-round {
  font-family: 'Material Icons Round';
  font-weight: normal;
  font-style: normal;
  font-size: 24px;
  vertical-align: middle;
  line-height: 1;
}

.list-section {
  margin: 1.5rem 0;
}

/* Pairs a section's h1 with a control (currently only the club-wide
   schedule's week selector — see hbnet-week-select) and reproduces the
   h1's own bottom margin at the row level since the h1 itself sits flush
   inside it. Stacked by default — on a phone the selector sits below the
   heading rather than squeezing onto the same line — and only placed
   beside it from 740px, this site's general mobile/desktop breakpoint
   (see the h1 rule above). */
.list-section-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.list-section-header h1 {
  margin: 0;
}

@media (min-width: 740px) {
  .list-section-header {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
  }
}

.hbnet-week-nav {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  max-width: 100%;
}

.hbnet-week-select {
  border: 1px solid rgba(128, 128, 128, 0.5);
  border-radius: 0.375rem;
  padding: 0.375rem 0.625rem;
  font: inherit;
  font-size: 0.875rem;
  background: #fff;
  color: inherit;
  min-width: 0;
}

.hbnet-week-select:focus-visible {
  outline: 2px solid #b71c1c;
  outline-offset: 1px;
}

/* The prev/next pair reuses .icon-button's own circle. Sizing is repeated
   here (matching .icon-button's own 2rem exactly, rather than relying on
   it) so the enabled (<a class="icon-button hbnet-week-nav-arrow">) and
   disabled (<span class="hbnet-week-nav-arrow hbnet-week-nav-arrow--
   disabled">) variants always end up the same box regardless of which
   rule the cascade would otherwise prefer — the two only ever differed by
   which classes they carried, and .icon-button's own size lost that tie
   under plain class-selector specificity, which is what made a button
   visibly shrink the moment it disabled. The disabled variant only ever
   changes color, never the box. */
.hbnet-week-nav-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
}

.hbnet-week-nav-arrow--disabled {
  color: rgba(0, 0, 0, 0.26);
}

.hbnet-week-nav-today {
  font-size: 0.8125rem;
  font-weight: 600;
  color: #b71c1c;
  text-decoration: none;
  white-space: nowrap;
}

.hbnet-week-nav-today:hover {
  text-decoration: underline;
}

.list-section-title {
  font-size: 0.875rem;
  font-weight: 500;
  color: rgba(0, 0, 0, 0.6);
  margin: 0 0 0.5rem;
}

.item-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.item-list.nested {
  padding-left: 2.5rem;
}

.item-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}

.item-icon {
  color: rgba(0, 0, 0, 0.54);
  flex-shrink: 0;
}

.item-main {
  flex: 1;
  min-width: 0;
}

.avatar-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem;
  margin: 0.25rem -0.5rem;
  border-radius: 0.5rem;
}

a.avatar-row {
  color: inherit;
  text-decoration: none;
}

a.avatar-row:hover {
  background: rgba(0, 0, 0, 0.05);
}

.avatar-icon {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.06);
  color: rgba(0, 0, 0, 0.54);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.item-title {
  font-weight: 500;
}

.item-description,
.item-tags {
  color: rgba(0, 0, 0, 0.6);
  font-size: 0.875rem;
}

.item-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.25rem;
}

.tag {
  display: inline-flex;
  align-items: center;
  line-height: 1.4;
  color: inherit;
  background: rgba(0, 0, 0, 0.08);
  border-radius: 1rem;
  padding: 0.2rem 0.6rem;
  font-size: 0.75rem;
  font-weight: 600;
}

.tag-label {
  margin-inline: 0.6rem;
}

.icon-button {
  display: inline-flex;
  color: inherit;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.06);
  /* color: rgba(0, 0, 0, 0.54); */
  /* display: flex; */
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-width: 0;
  cursor: pointer;
  text-decoration: none;
}

.icon-button:hover {
  background: rgba(0, 0, 0, 0.2);
}

.item-summary {
  display: flex;
  align-items: center;
  width: 100%;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border: none;
  border-bottom: 1px solid rgba(128, 128, 128, 0.2);
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.expand-icon {
  color: rgba(0, 0, 0, 0.54);
  flex-shrink: 0;
  transition: transform 0.2s ease;
}

.item-summary[aria-expanded="true"] .expand-icon {
  transform: rotate(180deg);
}

.item-collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.25s ease;
}

.item-collapse.open {
  grid-template-rows: 1fr;
}

.item-collapse-inner {
  overflow: hidden;
}

/* Organigramm — drawn server-side as one SVG image
   (internal/organigram/svg.go) and embedded by /ansprechpartner.

   What used to live here was a whole layout engine: a fixed CSS grid canvas,
   two-column fan-out with pseudo-element connectors, a scaled inline
   "thumbnail", and a pannable/pinch-zoomable dialog. All of it is gone, and so
   is the link that used to open the image on its own. The backend owns the
   layout, the page only sizes a picture, and reading it closer is the
   browser's page zoom — which needs nothing from us on any device. */
.organigram {
  margin: 0 0 2rem;
}

.organigram-title {
  font-size: 1.125rem;
  font-weight: 500;
  color: #fff;
  background: #b71c1c;
  margin: 0;
  padding: 0.4rem 1rem;
  border-radius: 0.5rem 0.5rem 0 0;
}

.organigram-frame {
  border: 2px solid #b71c1c;
  border-top: none;
  border-radius: 0 0 0.5rem 0.5rem;
  background: #fff;
  overflow: hidden;
}

/* height: auto overrides the intrinsic height attribute on the <img>, so the
   chart scales down to the content column while keeping its aspect ratio —
   the attribute is there to reserve space before it loads, not to fix a size.
   Being an image, it scales as one piece; nothing inside it can reflow. */
.organigram-image {
  display: block;
  width: 100%;
  height: auto;
}


.contact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: 1rem;
}

.contact-card {
  border: 1px solid rgba(128, 128, 128, 0.3);
  border-radius: 0.5rem;
  padding: 1rem;
}

.contact-card h3 {
  margin-top: 0;
}

/* Articles */

.article-cards {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.article-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  border: 1px solid rgba(128, 128, 128, 0.3);
  border-radius: 0.5rem;
  padding: 1rem;
  color: inherit;
  text-decoration: none;
  transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.article-card:hover {
  border-color: rgba(183, 28, 28, 0.4);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}

.article-card-main {
  flex: 1;
  min-width: 0;
}

.article-card-main .item-tags {
  margin-top: 0.75rem;
}

.article-card-image {
  flex-shrink: 0;
  width: 8rem;
  height: 8rem;
}

.article-card-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0.5rem;
}

.empty-message {
  color: rgba(0, 0, 0, 0.6);
  font-style: italic;
}

.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  justify-content: center;
  margin: 1.5rem 0;
  max-width: 100%;
}

.page-ellipsis {
  padding: 0.375rem 0.25rem;
  color: rgba(0, 0, 0, 0.5);
}

.page-button {
  border: 1px solid rgba(128, 128, 128, 0.3);
  background: none;
  border-radius: 0.375rem;
  padding: 0.375rem 0.75rem;
  cursor: pointer;
}

.page-button.active {
  background: rgba(0, 0, 0, 0.08);
  font-weight: 600;
}

.article-content {
  line-height: 1.6;
  margin: 1.5rem 0;
}

.announcement-card {
  border: 1px solid rgba(128, 128, 128, 0.3);
  border-radius: 0.5rem;
  padding: 1rem;
  margin-bottom: 1rem;
}

/* Carousel */

.carousel {
  position: relative;
  margin: 1rem 0;
}

.carousel-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 0.5rem;
  scrollbar-width: none;
}

.carousel-track::-webkit-scrollbar {
  display: none;
}

.carousel-slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  position: relative;
}

/* 3:2 frame with a blurred, scaled-up copy of the image filling the
   letterboxed edges behind the sharp, contained foreground copy — shared
   with the fullscreen lightbox's .lightbox-frame/.lightbox-image-bg. */
.carousel-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: 0.5rem;
}

.carousel-slide-img {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  cursor: zoom-in;
}

.carousel-caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  padding: 0.5rem;
  font-size: 0.875rem;
  border-radius: 0 0 0.5rem 0.5rem;
}

.carousel-prev,
.carousel-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.4);
  color: #fff;
  border: none;
  border-radius: 50%;
  width: 2.5rem;
  height: 2.5rem;
  cursor: pointer;
  font-size: 1.25rem;
  line-height: 1;
}

.carousel-prev {
  left: 0.5rem;
}

.carousel-next {
  right: 0.5rem;
}

/* Team page */

.team-subtitle {
  font-weight: 300;
  margin-top: 0;
}

.team-image {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 0.5rem;
  background: rgba(0, 0, 0, 0.04);
}

.team-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  cursor: zoom-in;
}

.team-image-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: rgba(0, 0, 0, 0.6);
  padding: 1rem;
}

/* CSS-only handball.net widget tabs */

.hbnet-tabs {
  margin: 1.5rem 0;
}

.hbnet-tab-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.hbnet-tab-labels {
  display: flex;
  border-bottom: 1px solid rgba(128, 128, 128, 0.3);
  margin-bottom: 1rem;
}

.hbnet-tab-labels label {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.75rem;
  cursor: pointer;
  border-bottom: 2px solid transparent;
}

.hbnet-tab-panel {
  display: none;
}

#hbnet-tab-tabelle:checked ~ .hbnet-tab-labels label[for="hbnet-tab-tabelle"],
#hbnet-tab-spielplan:checked ~ .hbnet-tab-labels label[for="hbnet-tab-spielplan"] {
  border-bottom-color: currentColor;
  font-weight: 600;
}

#hbnet-tab-tabelle:checked ~ .hbnet-tab-panel[data-panel="tabelle"],
#hbnet-tab-spielplan:checked ~ .hbnet-tab-panel[data-panel="spielplan"] {
  display: block;
}

/* handball.net data: results list and standings table */

.hbnet-day-label {
  font-size: 0.875rem;
  font-weight: 600;
  color: rgba(0, 0, 0, 0.6);
  margin: 1.25rem 0 0.5rem;
}

.hbnet-day-label:first-child {
  margin-top: 0;
}

.hbnet-games {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* Same card as .article-card — border, radius, hover lift — so a day of
   games and the list of reports below it read as one page rather than two
   unrelated widgets. The layout differs (stacked, no image), which is why
   this isn't just .article-card reused. */
/* Mobile-first (matches this file's other breakpoints): time over the
   (now short-coded, so it's rarely more than a word or two — see
   TournamentLabel) league in a narrow left column, both team rows —
   name pulled tight against that row's own score, not spread across the
   card — filling the rest of the width in a right column. Restored to
   three stacked full-width lines (time, centered teams, league) from
   740px up, where the games list is narrow enough that a second column
   would leave the league name no room — see that breakpoint below. */
.hbnet-game-card {
  position: relative;
  display: grid;
  /* minmax(0, 1fr), not a bare 1fr: a grid track's implicit minimum size
     is content-based, so a long unbroken team name would otherwise widen
     this column (and the card with it) instead of being constrained
     enough for .hbnet-game-team-name's own ellipsis to ever kick in. */
  grid-template-columns: 9.5rem minmax(0, 1fr);
  gap: 0.3rem 0.75rem;
  border: 1px solid rgba(128, 128, 128, 0.3);
  border-radius: 0.5rem;
  /* The extra right padding (vs. every other side's 1rem) is a permanent
     gutter for the live dot below — kept on every card, not only a live
     one, so a long away-team name never sails into that corner on the one
     in three hundred cards that happens to be live right now, and so
     every card keeps exactly the same content width regardless of state,
     which the column alignment above depends on. */
  padding: 0.75rem 1.5rem 0.75rem 1rem;
  color: inherit;
  text-decoration: none;
  transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

/* A plain dot rather than a badge with a "Live" label: the score already
   turns the same red the moment a game goes live (see the state-color
   rules further down), so this only needs to catch the eye before a
   reader has even reached the score, not repeat what it already says. */
.hbnet-game-card--live::before {
  content: "";
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: #c62828;
}

.hbnet-game-card:hover {
  border-color: rgba(183, 28, 28, 0.4);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}

/* Top of the left column, the league tag directly below it (see
   .hbnet-game-card .item-tags just below) — from 740px up this instead
   sits above the centered teams line on its own, pulled tight against it
   (see that breakpoint's own margin-bottom). */
.hbnet-game-time {
  grid-column: 1;
  grid-row: 1;
  font-size: 0.8rem;
  color: rgba(0, 0, 0, 0.6);
}

/* Bottom of the left column, under the kickoff time — see
   .hbnet-game-time just above. */
.hbnet-game-card .item-tags {
  grid-column: 1;
  grid-row: 2;
  margin-top: 0;
  align-self: start;
}

/* Competition names run long ("... Bezirks-Qualifikation NeckarAlb 19"),
   which on a phone turns the pill into a two-line block that dwarfs the
   score above it. One line, clipped — the dialog's Spielinfo tab spells
   the competition out in full. */
.hbnet-game-card .tag {
  max-width: 100%;
  min-width: 0;
}

.hbnet-game-card .tag-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The right column, spanning both of the left column's rows (time, league)
   and centered against their combined height — each team still gets its
   own row (name, crest, that team's own score) rather than a cramped
   3-column layout squeezed onto a narrow screen, but both rows now sit in
   this one right-hand column instead of spanning the card's full width,
   and each name is right-aligned so it reads right up against its own
   score (see .hbnet-game-team-name below) instead of sitting flush left
   with a gap of empty card before the score. Desktop turns the two rows
   into one, colon-separated line instead (see the media query below),
   back to spanning the full card width there. */
.hbnet-game-teams {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.hbnet-game-score-separator {
  display: none;
}

.hbnet-game-team {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.hbnet-game-team-name {
  flex: 1;
  min-width: 0;
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hbnet-game-team-score {
  flex-shrink: 0;
  min-width: 1.25rem;
  text-align: right;
  font-weight: 700;
}

/* The club's own team (SV Vaihingen), wherever it appears — not just the
   one team a given page belongs to. */
.hbnet-game-team--own .hbnet-game-team-name {
  font-weight: 600;
}

@media (min-width: 740px) {
  /* Back to three stacked full-width lines (time, centered teams, league)
     — the two-column layout below 740px (see .hbnet-game-card's own doc
     comment) needs a narrow screen to make sense of; from here the games
     list already has enough room for the league name in full on its own
     line, without needing a second column to free space for it. */
  .hbnet-game-card {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }

  /* Pulled tight against the centered teams line below it, so the two
     read as one block rather than three evenly spaced lines. */
  .hbnet-game-time {
    grid-column: auto;
    grid-row: auto;
    margin-bottom: -0.25rem;
  }

  .hbnet-game-card .item-tags {
    grid-column: auto;
    grid-row: auto;
    align-self: auto;
  }

  /* Fixed-width outer columns (rather than centering each row's group as
     a shrink-wrapped unit) so the score/separator always land in the same
     spot regardless of how long either team's name is — otherwise every
     row's score column drifts left/right with its own name lengths. */
  .hbnet-game-teams {
    grid-column: auto;
    grid-row: auto;
    align-self: auto;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 0.4rem;
  }

  .hbnet-game-score-separator {
    display: inline;
    font-weight: 700;
  }

  .hbnet-game-team-name {
    text-align: right;
  }

  /* Mirror the away team's children (name, crest, score in DOM order) so
     its score — like the home team's — ends up innermost, next to the
     separator, instead of the crest and name trailing it. */
  .hbnet-game-team--away {
    flex-direction: row-reverse;
  }

  .hbnet-game-team--away .hbnet-game-team-name {
    text-align: left;
  }
}

/* From here up there's room to fit a game's kickoff time, its competition
   and its teams/score on one line instead of three stacked ones (time
   above, teams centered, tag on its own line below — what the 740px tier
   above alone still renders, since between 740-899px the games list sits
   beside the right-nav column with too little width left for a third
   column). Below 900px a longer competition name already loses a line to
   itself for the same reason a game only gets one line from 740px up:
   there's nowhere left to put it without crowding the score.

   Fixed (not content-sized) widths for the time and league columns — not
   just 1fr either side of the teams block — for the same reason
   .hbnet-game-teams' own grid-template-columns two rules above is
   1fr auto 1fr rather than shrink-wrapped: every card using the exact
   same column widths is what keeps a game's time, league and teams lined
   up with every other game's all the way down the list, not merely tidy
   within its own card. The league column is wide enough that a real
   competition name ("weibliche B-Jugend Bezirksklasse Staffel 1") usually
   fits without the ellipsis .tag-label still falls back to for the rare
   longer one, rather than every name competing for space with the score.

   Visual order (time, league, teams) differs from DOM order (time, teams,
   league — see hbnet_games.html's own doc comment on why the tag comes
   last in the markup) purely via the `order` property below, so the
   teams block still reads right after the time in a screen reader or with
   CSS off, only its drawn position moves. */
@media (min-width: 900px) {
  .hbnet-game-card {
    display: grid;
    grid-template-columns: 4.25rem 17rem 1fr;
    align-items: center;
    gap: 0.75rem;
  }

  .hbnet-game-time {
    order: 1;
    margin-bottom: 0;
  }

  .hbnet-game-card .item-tags {
    order: 2;
  }

  .hbnet-game-teams {
    order: 3;
  }
}

/* State colors: finished games read as plain (already-black) text; a live
   game is the one state that needs to grab attention; upcoming games are
   muted since there's no result yet. */
.hbnet-game-card--post .hbnet-game-team-score,
.hbnet-game-card--post .hbnet-game-score-separator {
  color: rgba(0, 0, 0, 0.87);
}

.hbnet-game-card--live .hbnet-game-team-score,
.hbnet-game-card--live .hbnet-game-score-separator {
  color: #c62828;
}

.hbnet-game-card--pre .hbnet-game-team-score,
.hbnet-game-card--pre .hbnet-game-score-separator {
  color: rgba(0, 0, 0, 0.4);
}

.hbnet-full-link {
  display: block;
  text-align: center;
  padding: 0.75rem;
  margin-top: 0.5rem;
  font-weight: 600;
}

/* Matches .page-button's outline styling — see games-collapse.js, which
   toggles [hidden] on this rather than removing/re-adding it. The explicit
   [hidden] override below is required, not defensive: this rule's own
   "display: block" would otherwise outrank the browser default of
   "[hidden] { display: none }" (same specificity, author style wins),
   silently defeating button.hidden = true. */
.hbnet-games-more-button[hidden] {
  display: none;
}
.hbnet-games-more-button {
  display: block;
  width: 100%;
  border: 1px solid rgba(128, 128, 128, 0.3);
  background: none;
  border-radius: 0.375rem;
  padding: 0.625rem;
  margin-top: 0.75rem;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  /* Same hover treatment as .hbnet-game-card, so the button reads as
     another clickable row in the same list rather than a plain form
     control that happens to sit at the bottom of it. */
  transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.hbnet-games-more-button:hover {
  border-color: rgba(183, 28, 28, 0.4);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}

.hbnet-attribution {
  font-size: 0.75rem;
  color: rgba(0, 0, 0, 0.5);
  margin: 0.75rem 0 0;
}

/* Credit on the left, full-schedule link on the right. Both elements keep
   the standalone styling they need elsewhere (the game dialog uses each on
   its own), so the row strips their block-level margins and padding here
   rather than in the base rules above. Wraps to two lines when the pair
   won't fit. */
.hbnet-games-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 1rem;
  margin-top: 0.75rem;
}

.hbnet-games-footer .hbnet-attribution {
  margin: 0;
}

.hbnet-games-footer .hbnet-full-link {
  padding: 0;
  margin: 0;
  text-align: right;
}

.hbnet-table-wrap {
  overflow-x: auto;
}

.hbnet-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.hbnet-table th,
.hbnet-table td {
  padding: 0.4rem 0.5rem;
  text-align: center;
  white-space: nowrap;
  border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}

/* Specificity note: ".hbnet-table th/td" above pairs a class with an
   element selector, which outweighs a single class — these need the same
   pairing to actually win the cascade rather than silently losing to the
   centered default. */
.hbnet-table th.hbnet-table-rank,
.hbnet-table td.hbnet-table-rank,
.hbnet-table th.hbnet-table-team-header,
.hbnet-table td.hbnet-table-team {
  text-align: left;
}

.hbnet-table-team {
  white-space: normal;
}

.hbnet-team-logo {
  width: 1.25rem;
  height: 1.25rem;
  object-fit: contain;
  vertical-align: middle;
  margin-right: 0.4rem;
  flex-shrink: 0;
}

/* Inside the flex .hbnet-game-team, spacing comes from its own `gap`
   instead — the table's margin-right would double up next to it. */
.hbnet-game-team .hbnet-team-logo {
  margin-right: 0;
}

.hbnet-table-own {
  font-weight: 600;
  background: rgba(0, 0, 0, 0.05);
}

/* The association's footnote arrives as one string with newlines in it. */
.hbnet-table-footer {
  font-size: 0.75rem;
  color: rgba(0, 0, 0, 0.6);
  white-space: pre-line;
  margin: 0.75rem 0 0;
}

.hbnet-updated {
  font-size: 0.75rem;
  color: rgba(0, 0, 0, 0.5);
  margin: 0.75rem 0 0;
}

/* Fullscreen game dialog (handball.net game detail) */

/* Fullscreen on a phone, where a centred box would only waste edges; see
   the min-width query below for the contained panel larger screens get.
   The dialog itself never scrolls — it is a column whose last row
   (.game-panels) is the single scrolling region, so the score and the tab
   strip above it stay put. Everything from here down to .game-panels only
   exists to carry that constraint through the nesting: a flex child won't
   shrink below its content unless it is told min-height: 0, and without
   that the panels would grow the whole column instead of scrolling.

   Those children use `flex: 1 1 auto` rather than the usual `flex: 1`
   (= `1 1 0%`): a zero basis makes each box contribute nothing to its
   parent's intrinsic height, which would leave the dialog stuck at its
   max-height even on a short tab like Halle. An auto basis carries the real
   content height up the chain, so the box shrinks to fit a short tab and
   only starts scrolling once a long one (the ticker) hits max-height. */
.game-dialog {
  border: none;
  padding: 0;
  margin: 0;
  width: 100vw;
  max-width: 100vw;
  /* Same reasoning as .image-lightbox: dvh tracks the viewport actually
     visible on mobile, where vh would include the space under the
     address bar and push the close button off-screen. */
  height: 100dvh;
  max-height: 100dvh;
  background: #fff;
  color: inherit;
  /* fixed, not relative: both establish the containing block the close
     button's own `position: absolute` anchors to (see .game-dialog-close),
     so that part works identically either way — but unlike relative, fixed
     is also what makes the UA stylesheet's `inset: 0; margin: auto` (never
     overridden below, just the position value) actually centre the dialog
     against the viewport. relative instead renders the dialog at its
     normal position in page flow, which happens to coincide with the
     viewport only when the page is scrolled to the very top — confirmed
     2026-10-03: the dialog opened detached from the viewport, partly
     above it, as soon as the page behind it was scrolled down first. */
  position: fixed;
  /* Hidden until opened, exactly as .image-lightbox does it: the browser's
     own stylesheet hides a closed <dialog> with `display: none`, which a
     plain `display: flex` here would override — leaving an empty box parked
     at the foot of every page. The flex layout therefore only applies while
     the dialog is actually open. */
  display: none;
  flex-direction: column;
  overflow: hidden;
}

.game-dialog[open] {
  display: flex;
}

@media (min-width: 800px) {
  .game-dialog {
    width: min(56rem, 92vw);
    max-width: min(56rem, 92vw);
    /* fit-content, not auto: the UA stylesheet centres a modal dialog with
       `inset: 0; margin: auto`, and for an absolutely positioned box with
       both edges pinned, `height: auto` resolves by stretching to fill —
       leaving a mostly-empty panel on a short tab. fit-content restores the
       shrink-to-fit the UA default (also fit-content) intends, so the box
       sizes to the active tab and max-height caps a long one (the ticker),
       which then scrolls inside .game-panels. */
    height: fit-content;
    max-height: max(48rem, 84dvh);
    /* Anchored to a fixed distance from the top, not vertically centred:
       with `margin: auto` a centred box redistributes every height change
       across both edges, so switching from a short tab to a tall one shifted
       the score and the tab strip up the screen and back down again. A fixed
       top margin with `auto` below pins the top edge and lets the box grow
       downwards instead, so the header stays put whatever the active tab.
       8dvh + 84dvh max-height leaves a matching gap at the bottom. */
    margin: 8dvh auto auto;
    border-radius: 0.75rem;
    box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.3);
  }
}

.game-dialog::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

/* Set on <html> while the dialog is open — the dialog itself scrolls, the
   page behind it must not. */
.game-dialog-open {
  overflow: hidden;
}

/* body is pinned in place with `position: fixed` plus a negative `top`
   equal to however far the page had scrolled (set inline by
   game-dialog.js, which is also what undoes this and jumps scroll back to
   that same offset on close) rather than relying on `overflow: hidden`
   alone, the way the rule above still does for desktop. Two real mobile
   bugs this works around that overflow alone doesn't: iOS Safari's
   `overflow: hidden` on body never actually stops a touch-drag from
   scrolling it, and recalculating `100dvh`-sized `position: fixed`
   descendants (the dialog itself) against a layout viewport that's still
   mid-scroll, confirmed 2026-10-03, left the dialog visibly shifted
   toward the top of the screen rather than filling it. Pinning body
   first removes the scroll these depend on before the dialog ever opens,
   rather than opening the dialog and separately trying to freeze scroll
   under it. left/right 0 (not width: 100%) keeps it full-bleed without
   fighting any padding/border-box differences. */
.game-dialog-open body {
  position: fixed;
  left: 0;
  right: 0;
}

/* Positioned over the header rather than laid out in the column, so it
   keeps its corner without reserving a row; .game-detail-header's side
   padding keeps the centred title clear of it. */
.game-dialog-close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.06);
  color: inherit;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.game-dialog-close:hover {
  background: rgba(0, 0, 0, 0.12);
}

.game-dialog-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* `margin: 0 auto` (to centre this below max-width) is an auto cross-axis
     margin, which switches off the stretch a column flex child would
     otherwise get — so the width has to be stated, and border-box keeps the
     padding below inside it rather than overflowing the dialog. */
  box-sizing: border-box;
  width: 100%;
  max-width: 60rem;
  margin: 0 auto;
  /* No bottom padding: .game-panels supplies its own, inside the scroll
     area, so the last row of a long tab can reach the bottom edge. */
  padding: 1rem 1rem 0;
}

.game-detail {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.game-detail-header {
  flex: 0 0 auto;
  text-align: center;
  /* Room for the close button, mirrored on the left so the title stays
     optically centred. */
  padding: 0 3rem 1rem;
  border-bottom: 1px solid rgba(128, 128, 128, 0.3);
}

.game-detail-tournament {
  font-size: 0.875rem;
  color: rgba(0, 0, 0, 0.6);
  margin: 0 0 1rem;
}

.game-detail-score {
  display: grid;
  /* 1fr for each side and auto in the middle keeps the score centered on
     the dialog regardless of how lopsided the two team names are. */
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
}

.game-detail-side {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
}

.game-detail-logo {
  width: 3.5rem;
  height: 3.5rem;
  object-fit: contain;
}

.game-detail-team-name {
  font-size: 1rem;
}

.game-detail-side--own .game-detail-team-name {
  font-weight: 700;
}

.game-detail-goals {
  font-size: 2rem;
  font-weight: 500;
  white-space: nowrap;
}

.game-detail-goals-separator {
  margin: 0 0.35rem;
}

.game-detail-goals--pre {
  color: rgba(0, 0, 0, 0.4);
}

.game-detail-goals--live {
  color: #b71c1c;
}

.game-detail-state {
  font-size: 0.8125rem;
  color: rgba(0, 0, 0, 0.6);
  margin-top: 0.25rem;
}

/* The live refresh's progress bar (see game_partial.html): it fills over the
   five seconds between refreshes, so a running game reads as something that
   is keeping itself up to date rather than a score that happens to sit
   still. Deliberately small and under the "Live" label — it is a sign of
   life, not a control.

   Nothing restarts the animation: the whole .game-detail-result block is
   replaced by each refresh, so the bar is recreated mid-swap and starts
   over on its own, in step with the poll by construction. */
.game-live-refresh {
  width: 3rem;
  height: 2px;
  margin: 0.45rem auto 0;
  border-radius: 1px;
  background: rgba(0, 0, 0, 0.12);
  overflow: hidden;
}

.game-live-refresh-bar {
  display: block;
  width: 0;
  height: 100%;
  background: #b71c1c;
  animation: game-live-refresh 5s linear forwards;
}

@keyframes game-live-refresh {
  to {
    width: 100%;
  }
}

/* The request itself is in flight — the bar has finished its travel and is
   now waiting on handball.net. Fading it reads as "fetching" rather than
   leaving it looking stalled at the end. htmx puts this class on the
   element that issued the request, which is the block itself. */
.game-detail-result.htmx-request .game-live-refresh-bar {
  animation: none;
  width: 100%;
  opacity: 0.4;
}

/* No travelling bar, but the track stays: it still says the score updates
   by itself, which is the part that carries information. */
@media (prefers-reduced-motion: reduce) {
  .game-live-refresh-bar {
    animation: none;
    width: 100%;
    opacity: 0.5;
  }
}

/* Tabs */

.game-tabs {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.game-tab-labels {
  flex: 0 0 auto;
  display: flex;
  gap: 0.25rem;
  /* The five labels don't fit a phone; scroll them sideways rather than
     wrapping onto a second row or shrinking the text. */
  overflow-x: auto;
  border-bottom: 1px solid rgba(128, 128, 128, 0.3);
}

/* Sits below the scroll area, so the separator reads as the edge content
   is scrolling under rather than a line floating mid-dialog. */
.game-detail > .hbnet-attribution {
  flex: 0 0 auto;
  margin: 0;
  padding: 0.5rem 0 0.75rem;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
}

/* The dialog's only scrolling region. */
.game-panels {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* A panel sliding in starts offset sideways; clip that rather than let it
     become scrollable width. (Setting one axis to auto computes the other
     from `visible` to `auto`, so this has to be said explicitly.) */
  overflow-x: hidden;
  /* Keeps a flick past the end of a long ticker from scrolling the page
     behind the dialog. */
  overscroll-behavior: contain;
  padding: 1rem 0 2rem;
}

/* Switching tabs slides the incoming panel in from the side it lives on —
   left of the strip for a step back, right for a step forward — so a swipe
   is answered by content moving the same way the finger did. Only the
   arriving panel animates: the two have different heights, and holding both
   on screen to cross-slide them would mean overlaying one on the other and
   animating the dialog's height along with it. */
@keyframes game-panel-enter-forward {
  from {
    opacity: 0;
    transform: translateX(2rem);
  }
}

@keyframes game-panel-enter-back {
  from {
    opacity: 0;
    transform: translateX(-2rem);
  }
}

.game-panel--enter-forward {
  animation: game-panel-enter-forward 0.24s ease-out;
}

.game-panel--enter-back {
  animation: game-panel-enter-back 0.24s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .game-panel--enter-forward,
  .game-panel--enter-back {
    animation: none;
  }
}

.game-tab {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.75rem 1rem;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  color: inherit;
  font: inherit;
  white-space: nowrap;
  cursor: pointer;
}

.game-tab .material-icons {
  font-size: 1.125rem;
}

.game-tab[aria-selected='true'] {
  border-bottom-color: currentColor;
  font-weight: 600;
}

/* Info tab */

.game-facts {
  display: grid;
  grid-template-columns: minmax(7rem, max-content) 1fr;
  gap: 0.5rem 1rem;
  margin: 0 0 1.5rem;
}

.game-facts dt {
  color: rgba(0, 0, 0, 0.6);
  font-size: 0.875rem;
}

.game-facts dd {
  margin: 0;
}

.game-inline-icon {
  font-size: 1rem;
  vertical-align: text-bottom;
}

/* The Ort row is a plain .avatar-row, so it inherits the site's own icon +
   title + description layout and hover tint; these two rules are all it
   adds. align-items: start keeps the pin level with the hall's name rather
   than drifting to the middle of a three-line address. */
.game-venue {
  align-items: start;
  /* inline-flex rather than avatar-row's block-level flex, so the row is
     only as wide as its contents: stretched across the full dd, the
     external-link icon ended up stranded at the dialog's right edge, far
     from the address it belongs to. max-width keeps a long address from
     pushing past the dialog on a narrow screen. */
  display: inline-flex;
  max-width: 100%;
  /* Cancels avatar-row's negative side margins on the right only: the left
     one aligns the icon with the other dd values and is wanted, the right
     one would let the hover tint bleed past the dialog's padding. */
  margin-right: 0;
}

/* Without this the icon would still be pushed away by .item-main's `flex: 1`
   whenever the row does reach its max-width. */
.game-venue .item-main {
  flex: 0 1 auto;
}

.game-venue-external {
  flex-shrink: 0;
  font-size: 1.125rem;
  color: rgba(0, 0, 0, 0.4);
}

/* Aufstellung tab */

.game-lineups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: 2rem;
}

.game-lineup-subheading {
  font-size: 0.875rem;
  text-transform: uppercase;
  color: rgba(0, 0, 0, 0.6);
  margin: 1.5rem 0 0.5rem;
}

/* The stat columns hold at most a two-digit count, so they only need to be
   wide enough for their icon header. */
.game-lineup-table th.game-lineup-stat,
.game-lineup-table td.game-lineup-stat {
  width: 2.25rem;
  text-align: center;
}

.game-lineup-officials {
  list-style: none;
  margin: 0;
  padding: 0;
}

.game-lineup-officials li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.4rem 0;
  border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}

.game-lineup-number {
  /* auto, not a fixed basis: "Offizieller A/B/C" (schedule.Person.Position
     for a team official — see report_lineup.go) needs more room than a
     fixed narrow column gave it, which wrapped the label across two lines
     with plenty of row width still free. Sized to its own content instead,
     with nowrap so it never wraps internally again regardless of label
     length; .game-lineup-name's own flex-wrap still lets the name drop to
     its own line first if a row ever truly runs out of width. */
  flex: 0 0 auto;
  white-space: nowrap;
  color: rgba(0, 0, 0, 0.6);
  font-variant-numeric: tabular-nums;
}

.game-lineup-name {
  flex: 1 1 8rem;
  min-width: 0;
}

/* Liveticker tab */

.game-ticker {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* A 3px left edge marks which side an event belongs to (coloured by the
   modifiers further down), so the ticker can be skimmed without reading
   every message. Neutral entries — the whistles, the final score — keep the
   same edge in transparent rather than dropping it, so every row's time and
   icon stay on one vertical line. */
.game-ticker-event {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.65rem 0 0.65rem 0.5rem;
  border-bottom: 1px solid rgba(128, 128, 128, 0.2);
  border-left: 3px solid transparent;
}

/* Icon, time and score share one line; the message gets the full width
   below it, so a long one wraps across the row rather than inside a narrow
   middle column. */
.game-ticker-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* Shared by the ticker's per-event marker and the Aufstellung tables'
   column headers, so the same event always looks the same in both. */
.game-event-icon {
  font-size: 1.125rem;
  line-height: 1;
  flex: 0 0 auto;
  color: rgba(0, 0, 0, 0.55);
}

/* The one EventIcon.SVG icon (see layout.html's sprite): an <svg> has no
   intrinsic size of its own the way a font glyph does, so this gives it
   the same box every other .game-event-icon renders at (1em of its own
   font-size, inherited from the rule above) — color above becomes the
   SVG's own currentColor fill/stroke, so a variant or the ticker's
   goal-row override (.game-ticker-event--goal .game-event-icon below)
   recolors it exactly like a glyph. */
svg.game-event-icon {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: middle;
}

/* The three card types are one glyph, the round face's solid "rectangle" —
   which the font draws lying down, so it is stood upright here to read as a
   card. The colour is the only other thing telling the three apart. */
.game-event-icon--yellow,
.game-event-icon--red,
.game-event-icon--blue {
  display: inline-block;
  transform: rotate(90deg);
}

.game-event-icon--yellow {
  color: #f9a825;
}

.game-event-icon--red {
  color: #c62828;
}

.game-event-icon--blue {
  color: #1565c0;
}

.game-ticker-event--goal .game-event-icon {
  color: rgba(0, 0, 0, 0.8);
}

.game-ticker-time {
  color: rgba(0, 0, 0, 0.6);
  font-variant-numeric: tabular-nums;
  font-size: 0.875rem;
}

/* Left-aligned with the icon above it rather than indented past it: the
   message is the row's content and wants the full width, which is the
   point of giving it its own line. */
.game-ticker-message {
  line-height: 1.35;
}

.game-ticker-score {
  /* Pushed to the right of the head row. Every part inside is fixed-width,
     so the crests and the colon land on the same x on every row. */
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
  color: rgba(0, 0, 0, 0.6);
  font-variant-numeric: tabular-nums;
  /* Keeps the number off the panel's scrollbar, which sits right at this
     edge — .game-panels has no horizontal padding of its own. */
  padding-right: 0.5rem;
}

.game-ticker-crest {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.125rem;
  height: 1.125rem;
}

/* Greyed back by default: on a goal only the side that scored keeps its
   colours, and on an entry that scored for nobody (a whistle, a timeout)
   both stay muted — so colour in this column always means "these are the
   ones who just scored". */
.game-ticker-crest img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  filter: grayscale(1);
  opacity: 0.45;
}

.game-ticker-crest--scored img {
  filter: none;
  opacity: 1;
}

/* Each half is two digits wide — a team's score stays under 100 — and is
   aligned towards the colon, so a one-digit score doesn't shift the colon
   or the crests out of line with the rows around it. */
.game-ticker-score-home,
.game-ticker-score-away {
  display: inline-block;
  min-width: 2ch;
}

.game-ticker-score-home {
  text-align: right;
}

.game-ticker-score-away {
  text-align: left;
}

/* Only the half that just went up, so a glance at the ticker says which
   side scored without reading the message. */
.game-ticker-score--scored {
  font-weight: 600;
  color: rgba(0, 0, 0, 0.87);
}

.game-ticker-event--home {
  border-left-color: rgba(0, 0, 0, 0.35);
}

.game-ticker-event--away {
  border-left-color: #b71c1c;
}

@media (max-width: 600px) {
  .game-detail-logo {
    width: 2.5rem;
    height: 2.5rem;
  }

  .game-detail-team-name {
    font-size: 0.875rem;
  }

  .game-detail-goals {
    font-size: 1.5rem;
  }

  .game-facts {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }

  .game-facts dd {
    margin-bottom: 0.75rem;
  }
}

/* Site shell: mobile app bar + drawer, desktop banner, left/right nav, footer */

.mobile-appbar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 1rem;
  border-bottom: 1px solid rgba(128, 128, 128, 0.3);
  position: sticky;
  top: 0;
  background: #fff;
  z-index: 20;
}

.mobile-appbar-title {
  flex: 1;
  font-size: 1.25rem;
  font-weight: 500;
}

.menu-button {
  cursor: pointer;
  display: flex;
}

.drawer-toggle {
  display: none;
}

.drawer-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.3);
  z-index: 30;
}

.mobile-drawer {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 288px;
  max-width: 85vw;
  background: #fff;
  transform: translateX(-100%);
  transition: transform 0.2s ease;
  z-index: 31;
  overflow-y: auto;
  padding: 1rem;
}

.drawer-toggle:checked ~ .drawer-backdrop {
  display: block;
}

.drawer-toggle:checked ~ .mobile-drawer {
  transform: translateX(0);
}

.page-frame {
  max-width: 1280px;
  margin: 0 auto;
  background: #fff;
  /* Establishes a new block formatting context so .banner's own vertical
     margins (used to create the white gap above/below it) are contained
     within this box instead of collapsing out and exposing the page's grey
     background behind them. */
  display: flow-root;
}

.banner {
  display: none;
  position: relative;
  height: 130px;
  margin: 2.5rem auto;
}

.banner-logo {
  position: absolute;
  left: 60px;
  /* Centered on the stripe itself (which is shorter than .banner, offset by
     margin-top below), not on the taller .banner box — flexbox instead of
     the previous margin/border math, which centered on the wrong box. */
  top: 2.5rem;
  height: calc(130px - 2.5rem);
  display: flex;
  align-items: center;
  z-index: 1;
  line-height: 0;
}

.banner-logo svg {
  width: 130px;
  height: 130px;
  border-radius: 50%;
  border: 10px solid white;
  box-sizing: border-box;
}

.banner-logo svg .bg {
  fill: #b71c1c;
}

.banner-logo svg .text-color {
  fill: #fff;
}

.banner-stripe {
  margin-top: 2.5rem;
  height: calc(130px - 2.5rem);
  width: 100%;
  background-color: #b71c1c;
}

.banner-title-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100%;
  color: #fff;
}

.banner-title {
  font-family: 'Roboto', sans-serif;
  font-weight: 700;
  font-size: 2.55rem;
  margin-top: -8px;
}

.banner-subtitle {
  font-family: 'Roboto', sans-serif;
  font-size: 1.6rem;
  letter-spacing: -2px;
  margin-top: -10px;
}

.site-body {
  display: flex;
  align-items: flex-start;
  max-width: 1280px;
  margin: 0 auto;
  background: #fff;
}

.content {
  flex: 1;
  min-width: 0;
  max-width: 960px;
  margin: 0 auto;
  padding: 1rem;
}

.left-nav,
.right-nav {
  flex-shrink: 0;
  display: none;
  padding: 1rem;
  box-sizing: border-box;
}

.left-nav {
  width: 256px;
}

.right-nav {
  width: 216px;
}

@media (min-width: 740px) {
  .right-nav {
    display: block;
  }
}

@media (min-width: 1280px) {
  .mobile-appbar,
  .drawer-backdrop,
  .mobile-drawer {
    display: none !important;
  }

  .banner {
    display: flex;
  }

  .left-nav {
    display: block;
  }
}

.nav-section {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin: 1rem 0;
}

.nav-section-label {
  font-size: 0.75rem;
  font-weight: 500;
  color: #6b7280;
  margin-bottom: 1rem;
}

.nav-button {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem;
  border-radius: 0.375rem;
  color: inherit;
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.02em;
}

.nav-button:hover {
  background: rgba(0, 0, 0, 0.05);
}

.nav-external-icon {
  font-size: 1rem;
  margin-left: -0.375rem;
  opacity: 0.6;
}

.team-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.06);
  flex-shrink: 0;
  overflow: hidden;
}

.team-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.nav-spacer {
  height: 1rem;
}

.right-nav img {
  max-width: 100%;
  height: auto;
}

.site-footer {
  display: flex;
  justify-content: center;
  gap: 1.5rem;
  padding: 1.5rem;
  border-top: 2px solid rgba(0, 0, 0, 0.12);
  max-width: 1280px;
  margin: 0 auto;
  box-sizing: border-box;
  background: #fff;
}

.site-footer a {
  color: #374151;
  text-decoration: none;
}

.site-footer a:hover {
  text-decoration: underline;
}

/* Site notice (cookie consent) banner.
   Opened via .show() rather than .showModal() — see site-notice.js for why:
   in short, a modal's page-covering ::backdrop can outlive its content
   being hidden by a cookie/ad blocker, leaving an invisible click-blocker
   over the whole site. A non-modal dialog has no backdrop, so it's
   positioned here as a fixed bottom banner instead of relying on the
   browser's centered-modal default placement. */

.site-notice {
  position: fixed;
  left: 1rem;
  right: 1rem;
  bottom: 1rem;
  max-width: 26rem;
  margin: 0 auto;
  border: none;
  border-radius: 0.5rem;
  padding: 1.5rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
}

.site-notice-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1.25rem;
  font-weight: 500;
  margin-bottom: 0.75rem;
}

.site-notice-optional {
  font-weight: 400;
  font-size: 0.8125rem;
  color: rgba(0, 0, 0, 0.6);
}

.site-notice-option {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
}

.site-notice-option input[type="checkbox"] {
  width: 2.5rem;
  height: 1.5rem;
  flex-shrink: 0;
}

.site-notice-actions {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 1rem;
}

.site-notice-button {
  padding: 0.625rem 1rem;
  border-radius: 1.5rem;
  border: 1px solid rgba(0, 0, 0, 0.2);
  background: #fff;
  cursor: pointer;
  font-size: 0.9375rem;
}

.site-notice-button.primary {
  background: #b71c1c;
  color: #fff;
  border-color: #b71c1c;
}

/* Image lightbox */

.image-lightbox {
  display: none;
  flex-direction: column;
  border: none;
  padding: 0;
  margin: 0;
  width: 100vw;
  height: 100vh;
  max-width: 100vw;
  max-height: 100vh;
  /* 100dvh tracks the browser's actual visible viewport (it shrinks while
     the mobile address/tab bar is shown), unlike plain vh which is based
     on the largest possible viewport. Without this, the dialog could be
     taller than what's actually visible, pushing the thumbnail strip below
     the fold with no way to reach it. Declared after the vh fallback so
     browsers without dvh support keep that fallback. */
  height: 100dvh;
  max-height: 100dvh;
  /* Safety net for the rare case content still doesn't fit (e.g. a very
     short landscape viewport): scroll the whole dialog rather than clip
     the thumbnail strip permanently off-screen. */
  overflow: unset;
  /* The dialog itself fills the whole viewport, leaving no gap for
     ::backdrop to paint into, so the dim-behind-the-content color has to
     live on the dialog's own background instead of on ::backdrop. Fully
     opaque (not translucent): some Chromium versions fail to composite a
     translucent modal <dialog> background over sibling content that has
     its own compositing layer (e.g. the carousel's overflow-x scroller),
     leaving that content fully visible through the "dimmed" overlay. */
  background: #000;
}

.image-lightbox[open] {
  display: flex;
}

.lightbox-main {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

/* .lightbox-stage is sized to exactly match the image frame (same
   aspect-ratio/max-width/max-height) so the corner-anchored controls,
   positioned absolute against the stage, hug the image itself instead of
   floating far away from it on wide viewports. */
.lightbox-stage {
  position: relative;
  max-width: 100%;
  max-height: 100%;
  aspect-ratio: 3 / 2;
}

.lightbox-frame {
  position: absolute;
  inset: 0;
  border-radius: 0.5rem;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.4);
  /* Slide + crossfade between images on navigation. JS toggles this off
     (transition: none) while instantly placing the incoming pane off to
     the side, then clears the override so the move back to center (or the
     outgoing pane's move away) actually animates. */
  transition: transform 0.35s ease, opacity 0.35s ease;
}

.carousel-slide-bg,
.lightbox-image-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(40px) brightness(0.6);
  /* transform: scale(1.15); */
}

.lightbox-image {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.lightbox-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  font-size: 1.5rem;
  line-height: 1;
  text-decoration: none;
}

.lightbox-icon-btn:hover {
  background: rgba(0, 0, 0, 0.7);
}

.lightbox-close {
  position: absolute;
  top: 0.75rem;
  left: 0.75rem;
}

.lightbox-actions {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  display: flex;
  gap: 0.5rem;
}

.lightbox-prev,
.lightbox-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
.lightbox-prev[hidden],
.lightbox-next[hidden] {
  display: none;
}

.lightbox-prev {
  left: 0.75rem;
}

.lightbox-next {
  right: 0.75rem;
}

/* Overlaid on the stage (rather than laid out as a flex sibling of
   .lightbox-main) so its height — which varies with each image's caption
   length, including having none at all — never changes .lightbox-main's
   own box. That box is what the ResizeObserver in lightbox.js watches to
   resize the stage, so letting the caption affect it caused the stage
   (and the buttons anchored to its corners) to visibly resize on every
   navigation. */
.lightbox-description {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  color: #fff;
  text-align: center;
  padding: 0.5rem 1.5rem;
  font-size: 0.9375rem;
  white-space: pre-line;
  background: rgba(0, 0, 0, 0.55);
  border-radius: 0 0 0.5rem 0.5rem;
}

.lightbox-thumbs {
  flex-shrink: 0;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: 0.75rem;
  scrollbar-width: none;
}

.lightbox-thumbs::-webkit-scrollbar {
  display: none;
}

/* Centering via auto margins on a max-content track (rather than
   justify-content: center directly on the scroll container) so the strip
   is centered when it's narrower than the viewport but, once it overflows,
   scrolling still reaches both the first and last thumbnail — browsers
   generally block scrolling past a flex container's centered start edge,
   permanently hiding the leading thumbnails behind un-reachable scroll. */
.lightbox-thumbs-track {
  display: flex;
  gap: 0.5rem;
  width: max-content;
  margin: 0 auto;
}

.lightbox-thumb {
  width: 4rem;
  height: 4rem;
  object-fit: cover;
  border-radius: 0.375rem;
  flex-shrink: 0;
  scroll-snap-align: center;
  cursor: pointer;
  opacity: 0.5;
  border: 2px solid transparent;
}

.lightbox-thumb.active {
  opacity: 1;
  border-color: #fff;
}

.hb-embed__title-container {
  display: none !important;
}

.hb-embed__schedule-list-item-date {
  font-weight: bold;
}

/* Forms — Probetraining request */

.button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  border: 1px solid rgba(128, 128, 128, 0.3);
  border-radius: 0.375rem;
  padding: 0.5rem 1rem;
  background: none;
  color: inherit;
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}

.button:hover {
  background: rgba(0, 0, 0, 0.06);
}

.button.primary {
  background: #b71c1c;
  border-color: #b71c1c;
  color: #fff;
}

.button.primary:hover {
  background: #8e1616;
}

.team-actions {
  margin: 1rem 0 1.5rem;
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  max-width: 40rem;
  margin: 1.5rem 0;
}

.form-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}

.form-row .form-field {
  flex: 1 1 12rem;
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

.form-field label {
  font-weight: 600;
  font-size: 0.9rem;
}

.form-field input[type="text"],
.form-field input[type="email"],
.form-field input[type="tel"],
.form-field input[type="number"],
.form-field select,
.form-field textarea {
  border: 1px solid rgba(128, 128, 128, 0.5);
  border-radius: 0.375rem;
  padding: 0.5rem 0.625rem;
  font: inherit;
  background: #fff;
  color: inherit;
  /* Without this a padded input overflows its flex column on mobile. */
  box-sizing: border-box;
  width: 100%;
}

.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible {
  outline: 2px solid #b71c1c;
  outline-offset: 1px;
}

.form-field textarea {
  resize: vertical;
}

.form-hint {
  font-size: 0.8rem;
  color: rgba(0, 0, 0, 0.6);
  font-weight: 400;
}

.form-error {
  font-size: 0.8rem;
  color: #b71c1c;
  font-weight: 600;
}

.form-fieldset {
  border: 1px solid rgba(128, 128, 128, 0.3);
  border-radius: 0.5rem;
  padding: 1rem;
  margin: 0;
}

.form-fieldset legend {
  font-weight: 600;
  font-size: 0.9rem;
  padding: 0 0.375rem;
}

.form-radio,
.form-checkbox {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  padding: 0.375rem 0;
  cursor: pointer;
  font-weight: 400;
}

.form-radio input,
.form-checkbox input {
  /* Nudges the control onto the first line of a wrapping label. */
  margin-top: 0.2rem;
  flex-shrink: 0;
}

.form-notice {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  border: 1px solid rgba(128, 128, 128, 0.3);
  border-left-width: 4px;
  border-radius: 0.5rem;
  padding: 0.875rem 1rem;
  margin: 0.5rem 0;
}

.form-notice p {
  margin: 0.25rem 0 0;
  font-size: 0.9rem;
}

.form-notice.success {
  border-left-color: #2e7d32;
  background: rgba(46, 125, 50, 0.06);
}

.form-notice.success .material-icons {
  color: #2e7d32;
}

.form-notice.warning {
  border-left-color: #e08600;
  background: rgba(224, 134, 0, 0.08);
}

.form-notice.warning .material-icons {
  color: #e08600;
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}

/* The honeypot must be unreachable for people but still submitted by a bot
   parsing the DOM — display:none would let smarter bots skip it, so it's
   moved off-screen instead. */
.form-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Form validation states */

.button[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.button.primary[disabled]:hover {
  background: #b71c1c;
}

/* Only fields the visitor has already touched get marked — probetraining-form.js
   sets aria-invalid, deliberately not :invalid, which would light up every
   required field the moment the page loads. */
.form-field input[aria-invalid="true"],
.form-field select[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
  border-color: #b71c1c;
}

/* An empty error slot must not reserve vertical space, or every field on the
   form gains a gap it never uses. */
.form-error:empty {
  display: none;
}

.higher-class-toggle {
  margin-top: 0.75rem;
}

.higher-class-toggle .form-hint {
  display: block;
  margin-top: 0.125rem;
}

/* Age-class marker next to a team name, shown only when the list mixes the
   visitor's own class with the one above it. */
.class-tag {
  margin-left: 0.5rem;
  font-size: 0.7rem;
  vertical-align: middle;
}
