/* All app colours are defined here, so they can be changed centrally.

   The three prediction-result colours are green / yellow / red - exact score, correct
   outcome, wrong. This is deliberately NOT the colour-blind-safe scheme it replaces
   (blue / orange / grey): green against red is the common confusion, so for a
   red-green colour-blind reader the hue channel now carries less than it did. Three
   things are kept to pay for that, and they should stay:
     - the points number is always printed next to the colour, so the colour is a
       second channel and never the only one;
     - the three backgrounds are set at clearly different lightnesses (yellow 0.80,
       green 0.58, red 0.39 relative luminance), so they stay separable in greyscale;
     - so are the three borders (amber 0.28, green 0.16, red 0.11), in the opposite
       order, which makes each background/border pair unique without relying on hue.
   Keep those three properties if the shades are ever retuned. */
:root {
  --app-nav-bg: #ffffff;
  --app-accent: #1d4ed8;
  --app-cell-exact-bg: #8fd9a8;      /* exact score */
  --app-cell-exact-border: #15803d;
  --app-cell-outcome-bg: #ffe6a1;    /* correct outcome */
  --app-cell-outcome-border: #d97706;
  --app-cell-wrong-bg: #e88f8f;      /* wrong or missing */
  --app-cell-wrong-border: #b91c1c;
  --app-neutral-bg: #e5e7eb;         /* a badge or chip that carries no verdict */
  --app-neutral-fg: #374151;
  --app-live-ok: #15803d;            /* finalist still alive */
  --app-live-out: #b91c1c;           /* finalist eliminated */
  --app-you-bg: #dbe7ff;             /* the reader's own row, wherever everyone is listed */
  --app-you-border: #1d4ed8;

  /* League-phase qualification zones. The colours are a second channel only:
     every zone also carries a heading row naming it. */
  --app-zone-direct: #15803d;        /* straight to the knockout phase */
  --app-zone-playoff: #1f6fc4;       /* knockout play-off */
  --app-zone-out: #9ca3af;           /* out */
  --app-table-head-bg: #ffffff;
  --app-table-rule: #dee2e6;
  --app-scroll-shadow: rgba(0, 0, 0, 0.2);   /* the edge shadow on a table that scrolls sideways */
  --app-focus-halo: #ffffff;                 /* the inner ring that lifts a focus outline off the page */


  /* Match-row layout. The score columns have a fixed width on every row, which
     is what keeps the boxes of different matches on the same vertical lines.
     The phone values are tightened further down, where the names need the room. */
  --match-score-width: 3.5rem;
  --match-separator-width: 1rem;
  --match-crest-size: 1.75rem;
  --match-gap: 0.5rem;
  --match-time-width: 7rem;
  --match-tie-pick-width: 11rem;
}

/* Dark mode. Bootstrap 5.3 recolours its own components off [data-bs-theme="dark"] on
   <html> - _Layout stamps it, or a script in <head> resolves "use device setting" into
   it before the first paint - so all that is left here is the app's own palette above.

   The three prediction-result colours keep the properties the comment at the top of this
   file asks for, measured against the dark page rather than the white one:
     - the backgrounds stay at three clearly different lightnesses in the same order
       (yellow 0.19, green 0.10, red 0.04 relative luminance), so greyscale still
       separates them;
     - the borders stay in the opposite order (amber 0.23, green 0.33, red 0.39), which
       keeps every background/border pair unique without leaning on hue;
     - and the points number is still printed next to the colour.
   Retune the shades if they look wrong on a real screen, but keep those three orderings. */
[data-bs-theme="dark"] {
  --app-nav-bg: #1a1d20;
  --app-accent: #6ea8fe;
  --app-cell-exact-bg: #2a6440;
  --app-cell-exact-border: #4caf74;
  --app-cell-outcome-bg: #96731a;
  --app-cell-outcome-border: #a97b12;
  --app-cell-wrong-bg: #6b2020;
  --app-cell-wrong-border: #f08a8a;
  --app-neutral-bg: #343a40;
  --app-neutral-fg: #dee2e6;
  --app-live-ok: #4ade80;
  --app-live-out: #f87171;
  --app-you-bg: #1e304f;
  --app-you-border: #6ea8fe;

  --app-zone-direct: #22c55e;
  --app-zone-playoff: #60a5fa;
  --app-zone-out: #6b7280;
  --app-table-head-bg: #212529;
  --app-table-rule: #495057;

  /* A shadow is invisible against a dark page, so the edge is drawn as a highlight
     instead - the same trick, with the light coming from the other side. */
  --app-scroll-shadow: rgba(255, 255, 255, 0.25);
  --app-focus-halo: #212529;
}

.app-navbar {
  background-color: var(--app-nav-bg);
}

/* The bar's heading: normally the competition emblem and the tournament's name, and
   the app icon with the site's name when there is no tournament to show. Both images
   are sized in pixels rather than rem on purpose - an image is a picture, and it should
   not grow and shrink with the root font the way the text beside it does. 30px is the
   smallest size at which the three result colours under the app icon's trophy still
   read as three colours; at the more usual 22px they collapse into one smudge.

   The name is data, so it can be any length. It truncates rather than wrapping to a
   second line - a.navbar-brand below sets word-break: break-all, which would otherwise
   chop a long name mid-word. The max-width is what keeps the burger up on the first row
   with it: the navbar's container wraps, so a brand allowed to fill the bar pushes the
   button underneath instead of being made to shrink. The reserve is the button's own
   width plus the brand's 1rem margin. */
.app-brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  max-width: calc(100% - 5rem);
  overflow: hidden;
}

.app-brand__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-brand__mark {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 6px;
}

/* The competition picker, where there is more than one competition to pick. A caret and
   nothing else: the emblem and the name beside it already say which one is showing, and a
   second label saying it again is the row this bar was cleared of in the first place.

   The brand gives up room for it. .app-brand above reserves the burger's width so that
   the button stays on the first row; this reserves the caret's as well, and only on the
   pages that have one. */
.app-brand-switch {
  flex: 0 0 auto;
  margin-right: auto;
}

.app-brand:has(+ .app-brand-switch) {
  max-width: calc(100% - 8rem);
}

/* The emblem is a transparent picture, not a tile, so it gets no rounded corners and
   no fixed width - crests are not all square. */
.app-brand__crest {
  flex: 0 0 auto;
  height: 30px;
  width: auto;
}

/* The Champions League emblem is pure black on transparency, which on the dark bar is
   black against near-black - the starball all but disappears. Inverting a monochrome
   picture turns it white and leaves the transparency alone, which is the reversed
   lockup UEFA prints on dark backgrounds anyway. A filter rather than a second file
   because there is only one thing to flip: one colour, no shading.

   This holds only while the emblem stays monochrome. A competition whose crest carries
   real colour would come out here as a photographic negative, and would need its own
   dark file instead. */
[data-bs-theme="dark"] .app-brand__crest {
  filter: invert(1);
}

/* Logout is a <button> inside a form, because signing out is a POST - but it has to
   read as the menu item next to it. .btn-link would paint it link-blue, and it used to
   be pinned to .text-dark instead, which was the same near-black as the rest of the bar
   only while the bar was always white. Point the button's own colour variables at the
   navbar's instead, so it follows whichever theme is showing, the way the real nav links
   already do. */
.navbar .nav-link.btn-link {
  --bs-btn-color: var(--bs-navbar-color);
  --bs-btn-hover-color: var(--bs-navbar-hover-color);
  text-decoration: none;
}

/* Menu icons. Phone and tablet only: below the lg breakpoint the navbar
   collapses into a vertical stack, where an icon gives each item something to
   aim at and the row has width to spare. On the wide bar the items sit
   shoulder to shoulder in one line, and the same icons only crowd it, so they
   are off by default and switched on in the media query below - the same
   breakpoint the group separators turn on their side at.

   Sized in em so they follow the link's own font size rather than a fixed
   pixel count, and nudged up a hair because a 16-square glyph sits optically
   low against lower-case text. `currentColor` in the sprite does the colouring,
   so hover, active and the dark dropdown all come for free. `flex-shrink: 0`
   stops a long label squeezing the icon flat in the stack. */
.nav-icon {
  display: none;
  width: 1em;
  height: 1em;
  margin-right: 0.5em;
  vertical-align: -0.125em;
  flex-shrink: 0;
}

/* Group separators in the navbar. Wide enough to read as a divider, never as a
   menu item of its own, which is why it carries no text and is hidden from
   screen readers. Below the lg breakpoint the navbar collapses into a vertical
   stack, where a vertical bar would be nonsense, so the same element is drawn
   as a horizontal rule across the stack instead. */
.nav-sep {
  display: flex;
  align-items: center;
  padding: 0 0.625rem;
}

.nav-sep__rule {
  display: inline-block;
  width: 1px;
  height: 1.25rem;
  background-color: var(--bs-border-color);
}

@media (max-width: 991.98px) {
  .nav-icon {
    display: inline-block;
  }

  .nav-sep {
    padding: 0.375rem 0;
  }

  .nav-sep__rule {
    width: 100%;
    height: 1px;
  }
}

/* A scored prediction: the tint says how good it was, the left border repeats it in
   a second, darker step so the three are still told apart in greyscale.

   background-color alone is not enough inside a Bootstrap table, which is where two of
   the three pages put these. `.table > :not(caption) > * > *` sets background-color
   itself, and at (0,1,3) it outscores a single class, so on a plain .table the tint was
   simply overwritten and only the 3px border survived - the "thin left stripe" the
   Participant page showed at every width. .table-striped is worse again: it paints its
   stripe as an `inset 0 0 0 9999px` box-shadow, which covers any background-color no
   matter who wins the cascade, so on Extras the tint was hidden on every other row.

   So feed Bootstrap its own variable instead of fighting it. --bs-table-bg-state is the
   first fallback in the box-shadow above, ahead of the striping variable, and nothing
   else in the app sets it (no table here uses .table-hover, the only other writer). The
   cell is then painted by Bootstrap, in our colour, over the stripe. background-color
   stays for the same classes used outside a table: the legend chips on all three pages
   and the per-participant list Matchday shows on a phone. */
.cell-exact {
  background-color: var(--app-cell-exact-bg);
  --bs-table-bg-state: var(--app-cell-exact-bg);
  border-left: 3px solid var(--app-cell-exact-border);
}

.cell-outcome {
  background-color: var(--app-cell-outcome-bg);
  --bs-table-bg-state: var(--app-cell-outcome-bg);
  border-left: 3px solid var(--app-cell-outcome-border);
}

.cell-wrong {
  background-color: var(--app-cell-wrong-bg);
  --bs-table-bg-state: var(--app-cell-wrong-bg);
  border-left: 3px solid var(--app-cell-wrong-border);
}

/* The reader's own line, wherever everyone is listed at once: Matchday's cards, the
   leaderboard, everyone's extras. Nine near-identical rows are searched by reading
   names, and the one row anybody is actually looking for is their own.

   Painted through --bs-table-bg-state for exactly the reason the .cell-* block above
   explains - .table-striped's box-shadow would otherwise cover a background-color on
   every other row, which is every other participant depending on where they sort. The
   left border is what survives the cases the tint cannot carry on its own: a list-group
   item inside a Matchday card sits on the card's own background, and in dark mode the
   tint is deliberately quiet.

   Deliberately not bold. Bold already means "total" in these tables - the leaderboard's
   points column, Matchday's day figure - and a bold name would be a third meaning for
   it on the same row. */
.row-you {
  background-color: var(--app-you-bg);
  --bs-table-bg-state: var(--app-you-bg);
  box-shadow: inset 3px 0 0 0 var(--app-you-border);
}

/* A table cell cannot carry the row's shadow - it would be drawn on all of them - so the
   first cell of the row carries it instead, and the rest only take the tint. */
tr.row-you {
  box-shadow: none;
}

/* An empty anchor laid over a whole row, so that tapping anywhere on a match goes to it.
   Bootstrap's .stretched-link would do the covering, but not the getting out of the way:
   the rows it is used on are grids, and an anchor left in flow takes a cell of its own and
   shunts everything after it one place along. Absolute takes it out of flow, which is what
   makes it free to sit anywhere among the row's children.

   It carries no text - the row underneath is the text - so its accessible name is an
   aria-label, and the row needs position: relative for the overlay to have something to be
   the size of. */
.row-link {
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* The row reacts, rather than the invisible thing over it: without this nothing on a
   phone acknowledges the tap, and on a mouse there is nothing to say the row leads
   anywhere. Keyboard focus lands on the anchor, which has no box of its own, so the
   outline is drawn round the row too. */
.row-link:hover::after,
.row-link:focus-visible::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--bs-secondary-bg);
  opacity: 0.35;
  pointer-events: none;
}

.row-link:focus-visible {
  outline: 2px solid var(--bs-focus-ring-color, currentColor);
  outline-offset: -2px;
}

tr.row-you > :first-child {
  box-shadow: inset 3px 0 0 0 var(--app-you-border);
}

.finalist-alive {
  color: var(--app-live-ok);
  font-weight: 600;
}

.finalist-out {
  color: var(--app-live-out);
  text-decoration: line-through;
}

/* Right and wrong, once the outcome is known. Deliberately not struck through:
   a struck-out name reads as "knocked out", which is a different statement. */
.pick-right {
  color: var(--app-live-ok);
  font-weight: 600;
}

.pick-wrong {
  color: var(--app-live-out);
}

/* What one prediction scored. Quiet on purpose: the prediction is what is being read,
   the number is the footnote - so it takes the neutral grey, not the "wrong" colour it
   used to borrow back when that colour happened to be grey too. */
.points-badge {
  display: inline-block;
  padding: 0 0.35rem;
  border-radius: 0.35rem;
  background-color: var(--app-neutral-bg);
  color: var(--app-neutral-fg);
  font-size: 0.8em;
  font-variant-numeric: tabular-nums;
}

html {
  font-size: 14px;
}

@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

/* Form fields are the one exception to the 14px root above.

   Safari on an iPhone zooms the page in whenever the field being tapped sets its
   text smaller than 16px, and it never zooms back out - so tapping a score box on
   Predict left the page magnified with half the round off the side of the screen.
   Confirmed on a real iPhone, 2026-09-06; no other browser does this, which is why
   testing on Android and a PC never showed it.

   The fix pins the fields alone rather than raising the root: 14px is deliberate
   everywhere else, and it is what lets a match row fit two names, two crests and
   two score boxes across a phone. The small variants are listed too - Bootstrap
   sets those to 0.875rem, which is under the threshold at any root size. */
@media (max-width: 767.98px) {
  .form-control,
  .form-control-sm,
  .form-select,
  .form-select-sm {
    font-size: 16px;
  }
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem var(--app-focus-halo), 0 0 0 0.25rem #258cfb;
}

html {
  position: relative;
  min-height: 100%;
}

body {
  margin-bottom: 60px;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
  color: var(--bs-secondary-color);
  text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
  text-align: start;
}
/* A match row: home name, crest, two score cells, crest, away name, mirrored
   around the centre. The crests sit between each name and the score boxes: the
   names are aligned towards the centre, so a crest on the outer edge would be
   stranded away from its own team whenever the name is short.

   Each row is its own grid, but every column except the two names has a fixed
   width, so the two 1fr name columns resolve identically on every row and the
   score cells line up down the page. A flex row cannot do that: flex-fill on
   both names puts the centre at the midpoint of whatever space the names leave
   over, which differs for every pair of teams.

   The crest sits in its own track rather than inside the name cell, so a team
   without a crest leaves a gap instead of shifting that row out of line. */
.match-row {
  display: grid;
  grid-template-columns:
    minmax(0, 1fr)
    var(--match-crest-size)
    var(--match-score-width)
    var(--match-separator-width)
    var(--match-score-width)
    var(--match-crest-size)
    minmax(0, 1fr);
  align-items: center;
  column-gap: var(--match-gap);
  row-gap: 0.25rem;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}

/* The admin pages put the kickoff in a leading column. */
.match-row--timed {
  grid-template-columns:
    var(--match-time-width)
    minmax(0, 1fr)
    var(--match-crest-size)
    var(--match-score-width)
    var(--match-separator-width)
    var(--match-score-width)
    var(--match-crest-size)
    minmax(0, 1fr);
}

.match-row__home {
  text-align: right;
}

/* Names wrap rather than truncate: it matters more that the team is readable
   than that every row is exactly one line high. */
.match-row__home,
.match-row__away {
  min-width: 0;
  overflow-wrap: break-word;
}

.match-row__score,
.match-row__sep {
  text-align: center;
}

/* Badges and the tie-winner select go on their own full-width line, so they
   can never widen a column and pull that row's score cells out of line. */
.match-row__extra {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

/* A day of the Predict or Fixtures page, folded and unfolded by the browser. The caret is
   drawn here for the same reason the match day's is - the summary's contents are
   block-level, so the browser's own marker would take a line above the date instead of
   sitting beside it - but centred rather than nudged down, this summary being one line
   tall.

   The day is the only thing on either page that folds, so there is never a caret inside a
   caret and never a second size of one. The rule is in PredictService and
   FixturesService. */
.day-fold > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  list-style: none;
  margin-top: 1.5rem;
}

.day-fold > summary::-webkit-details-marker {
  display: none;
}

.day-fold > summary::before {
  content: "";
  flex: 0 0 auto;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid var(--bs-secondary-color);
  border-bottom: 2px solid var(--bs-secondary-color);
  transform: rotate(-45deg);
}

.day-fold[open] > summary::before {
  transform: rotate(45deg);
}

/* Kickoff time as a heading over the matches that start at it, instead of
   repeating the same time on every row of the group. */
.match-time {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--bs-secondary-color);
  margin: 0.75rem 0 0.25rem;
}

/* Team crests. The empty span keeps the grid track occupied for a team that has
   no crest, so the rest of the row stays in its columns. */
.team-crest {
  display: inline-block;
  width: var(--match-crest-size);
  height: var(--match-crest-size);
  object-fit: contain;
  vertical-align: middle;
  flex: none;
}

/* A stacked match row: the two teams sit under each other, each on its own line.
   The lists on Home and Fixtures used to run "crest Home - crest Away" as one
   inline span, which broke at whatever point it happened to overflow and left
   the dash dangling at the end of a line. With one team per line there is no
   wrap point left to get wrong, and the fixed crest column lines the names up
   down the page. */
.match-stack {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 0.75rem;
}

.match-stack__meta {
  display: grid;
  justify-items: start;
  row-gap: 0.125rem;
}

.match-stack__teams {
  display: grid;
  min-width: 0;
  row-gap: 0.125rem;
}

/* One team: the crest column is fixed so every name starts at the same x. */
.match-stack__team {
  display: grid;
  grid-template-columns: var(--match-crest-size) minmax(0, 1fr);
  align-items: center;
  column-gap: 0.375rem;
}

/* Lists that show a result put it on the team's own line, the way a result is
   actually read, rather than as one "2:1" beside the pair. */
.match-stack--scored .match-stack__team {
  grid-template-columns: var(--match-crest-size) minmax(0, 1fr) 1.5rem;
}

.match-stack__name {
  min-width: 0;
  overflow-wrap: break-word;
}

/* ...but not inside a table. break-word lets a name's minimum width fall to a single
   character, and an auto-layout table takes that as permission to squeeze the Match
   column until "Arsenal" comes out one letter per line - which is what happens on
   Matchday around 800px and on Admin -> Matches at phone width. Nowrap makes the
   longest name the column's minimum instead, so the table grows past its wrapper and
   .table-responsive scrolls sideways: the same "room first, scrolling second" trade
   the extras table makes below. */
td .match-stack__name {
  overflow-wrap: normal;
  white-space: nowrap;
  /* ...and the name has to actually claim that width. The list layout above gives the
     name track minmax(0, 1fr) and min-width: 0 so it may shrink and wrap; combined with
     nowrap that is the worst of both, because the track collapses and the text simply
     overflows the cell - "Arsenal" printed straight over the column border and into the
     result. max-content makes the full name the minimum, which is what propagates up
     and makes the table widen the column instead. */
  min-width: max-content;
}

td .match-stack__team {
  grid-template-columns: var(--match-crest-size) max-content;
}

td .match-stack--scored .match-stack__team {
  grid-template-columns: var(--match-crest-size) max-content 1.5rem;
}

/* Nowrap makes the Match column exactly as wide as its longest name, which leaves
   table-sm's quarter-rem of padding as the only thing between "Arsenal" and the
   result next to it - they read as one run. Push the following column away here
   rather than padding the cell, so it works without either page having to put a
   class on the td. */
td .match-stack__teams {
  padding-inline-end: 1rem;
  border-inline-end: 1px solid var(--bs-border-color);
}

/* The cell the teams sit in gets the matching gap on the other side of that rule,
   so the result is not pressed against it. Padding on the block alone was not
   enough: the result sits vertically centred between the two team lines, which
   reads as belonging to them however much air is on either side. A line is what
   actually says "this column ended". */
td:has(> .match-stack__teams) {
  padding-inline-end: 0;
}
td:has(> .match-stack__teams) + td {
  padding-inline-start: 0.75rem;
}

.match-stack__score {
  font-weight: 600;
  text-align: right;
}

/* A sending-off, as score apps draw it: a small upright red card straight after the
   team's name, one per player. Sized in em so it follows the name's own text size,
   and a fixed red rather than a theme colour because a red card is red in both.
   The yellow one is the same card, heading the Players page's bookings column. */
.red-card,
.yellow-card {
  display: inline-block;
  width: 0.5em;
  height: 0.7em;
  margin-inline-start: 0.3em;
  vertical-align: -0.05em;
  background: #e53935;
  border-radius: 0.1em;
}

.yellow-card {
  background: #fdd835;
}

.red-card + .red-card {
  margin-inline-start: 0.15em;
}

/* The scorers are a fourth child of a three-column grid, so without this they are
   placed in the first column of an implicit second row - the meta column, which is
   auto-sized. A line like "Xavi Simons 9', Benjamin Sesko 31'" then becomes that
   column's width, and the team names, which sit in the only minmax(0, 1fr) track,
   are squeezed until "Slavia Praha" comes out one letter per line. Spanning the row
   puts the line under the match, where it reads, and leaves the columns alone.

   There are two of these now, one per side. They were tried inside the team block,
   directly under the team each belonged to, and that reads worse: it puts a line of
   names between the home score and the away score, and two digits that far apart stop
   being a scoreline. Under the row the score stays intact and the goals stay grouped. */
.match-stack__goals {
  grid-column: 1 / -1;
  margin-top: 0.25rem;
}

/* Only the first line needs the gap that separates it from the row above; the second
   sits tight under the first, so the pair reads as one block rather than two remarks. */
.match-stack__goals + .match-stack__goals {
  margin-top: 0;
}

/* The code that opens a scorer line. Slightly stronger than the names after it so the
   eye can run down the codes and find a side without reading the goals, and followed by
   a colon rather than a space, which is what keeps "FUL" from reading as a scorer. */
.match-stack__goals-team {
  font-weight: 600;
}

.match-stack__goals-team::after {
  content: ":";
}

/* Everyone's extras. Nothing in this table may wrap, which is what makes it overflow
   its .table-responsive wrapper and scroll sideways. Without it the table always fits,
   so it never scrolls, and the browser squeezes a column instead until a team name
   breaks mid-word - "Barca" as "Barc" over "a". Room first, scrolling second. */

/* A scroll container has to be a containing block as well, or absolutely positioned
   descendants are not clipped by it - per CSS 2.1 an overflow box does not clip a
   descendant whose containing block lies outside it.

   That is what made the whole Extras page scroll sideways on a phone, navbar and form
   included, rather than just the table. Bootstrap's .visually-hidden is
   position: absolute, and _MarkedValue emits one next to every tick and cross, so the
   wide "Everyone's extras" table is full of them. With no positioned ancestor between
   them and <html> - which site.css gives position: relative - they resolved against
   <html>, escaped the overflow, and kept their static position several hundred pixels
   to the right of the viewport. Each one is 1px wide and clipped to nothing, so nothing
   is visible out there; the document is simply that wide.

   Note the earlier theory written in TODO.md was wrong: there is no flex or grid
   ancestor to give a min-width: 0. The chain from <body> to the table is all block
   boxes. */
.table-responsive {
  position: relative;
}

/* A shadow on whichever edge has content beyond it, so a table that scrolls sideways
   says so. The two covers are painted in the page colour and scroll WITH the content
   (background-attachment: local); the two shadows stay put. At either end the cover
   sits exactly over the shadow and hides it, and a table that needs no scrolling hides
   both - which is why this needs no media query and no script. */
.scroll-shadows {
  background:
    linear-gradient(to right, var(--bs-body-bg) 30%, rgba(255, 255, 255, 0)) left center,
    linear-gradient(to left, var(--bs-body-bg) 30%, rgba(255, 255, 255, 0)) right center,
    radial-gradient(farthest-side at 0 50%, var(--app-scroll-shadow), rgba(0, 0, 0, 0)) left center,
    radial-gradient(farthest-side at 100% 50%, var(--app-scroll-shadow), rgba(0, 0, 0, 0)) right center;
  background-repeat: no-repeat;
  background-size: 2.5rem 100%, 2.5rem 100%, 0.9rem 100%, 0.9rem 100%;
  background-attachment: local, local, scroll, scroll;
}

/* Bootstrap paints every cell with the page colour, which would cover the shadows up.
   Transparent instead: the striped rows are a translucent overlay and look unchanged. */
.extras-table,
.table-transparent {
  --bs-table-bg: transparent;
}
.extras-table th,
.extras-table td {
  white-space: nowrap;
}

/* One predicted finalist. Flex rather than the .match-stack grid on purpose: that grid
   is built to let a name wrap, so it may shrink to nothing, and a cell that may shrink to
   nothing never widens the table - the text just spills over the next column instead.
   Flex items keep their content width, so the cell asks for the room it needs and the
   table overflows its wrapper, which is what makes the wrapper scroll.
   The crest is a fixed size either way, so the two names still line up. */
.extras-finalist {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

.extras-finalist + .extras-finalist {
  margin-top: 0.125rem;
}

/* "Who advances" rows: a fixed picker column keeps the selects aligned. */
.tie-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--match-tie-pick-width);
  align-items: center;
  column-gap: 0.5rem;
  row-gap: 0.25rem;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}

.tie-row__teams {
  min-width: 0;
  overflow-wrap: break-word;
}

.tie-row__pick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* On a phone every fixed column is taken out of the two name columns, so they
   all shrink together and the kickoff moves to a line of its own. */
@media (max-width: 575.98px) {
  :root {
    --match-score-width: 3.25rem;
    --match-separator-width: 0.75rem;
    --match-crest-size: 1.5rem;
    --match-gap: 0.375rem;
  }

  .match-row--timed {
    grid-template-columns:
      minmax(0, 1fr)
      var(--match-crest-size)
      var(--match-score-width)
      var(--match-separator-width)
      var(--match-score-width)
      var(--match-crest-size)
      minmax(0, 1fr);
  }

  .match-row__time {
    grid-column: 1 / -1;
  }

  .tie-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Standings table.

   The header stays put while the 36 rows scroll under it. This only works
   because the table is not wrapped in Bootstrap's .table-responsive: that sets
   overflow-x, which per spec forces overflow-y to auto as well, making the
   wrapper a scroll container that a sticky header would anchor to instead of
   the viewport. The navbar does not stick, so top: 0 is the right anchor. */
.standings thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background-color: var(--app-table-head-bg);
  /* A border-bottom on a sticky cell is dropped under border-collapse:
     collapse, so the underline is drawn as an inset shadow instead. */
  box-shadow: inset 0 -2px 0 var(--app-table-rule);
}

/* Every first cell reserves the width of the zone bar, header included, so the
   header row cannot sit a few pixels left of the rows underneath it. The zone
   classes then only change the colour, never the layout. */
.standings thead th:first-child,
.standings tbody tr > td:first-child {
  border-left: 4px solid transparent;
}

.standings tbody tr.zone-direct > td:first-child {
  border-left-color: var(--app-zone-direct);
}

.standings tbody tr.zone-playoff > td:first-child {
  border-left-color: var(--app-zone-playoff);
}

.standings tbody tr.zone-out > td:first-child {
  border-left-color: var(--app-zone-out);
}

/* The points column carries the ranking, so it is set off from the rest. No
   fixed widths anywhere else: pinning the number columns stops them absorbing
   surplus width, which auto layout then dumps entirely into the Team column. */
.standings__points {
  border-left: 1px solid var(--app-table-rule);
}

/* The row naming a zone, above the first team in it. Striping is painted as an
   inset shadow covering the whole cell, not as a background, so the shadow has
   to be cleared as well or an odd-numbered heading row comes out tinted. The
   row keeps an empty points cell so that column's rule runs unbroken. */
.standings__zone-head > td {
  background-color: var(--app-table-head-bg);
  box-shadow: none;
  padding-top: 0.75rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--bs-secondary-color);
}

/* Points over time - the detailed chart.

   One canvas in a box that scrolls sideways when the resolution asks for more width than the
   screen has. A pinned y axis was tried here first, as a second narrow canvas beside this
   one, and it inverted on a phone: the axis took the width and left the plot about fifty
   pixels. Two charts sharing a layout is a promise this stylesheet cannot keep on its own,
   so the axis scrolls with everything else - see TODO.md. */
.points-chart-scroll {
  position: relative;
  height: 26rem;
  overflow-x: auto;
  overflow-y: hidden;
}

/* Width is set in pixels from JavaScript: the number of ticks times what a tick needs. */
.points-chart-plot {
  position: relative;
  height: 100%;
}

/* The chart's own legend is off, so this is what says which line is whose. A bar rather
   than a square, because it also has to show the dash pattern the line is drawn with. */
.series-swatch {
  display: inline-block;
  flex: 0 0 auto;
  width: 1.5rem;
  height: 0.25rem;
  border-radius: 0.125rem;
}

/* The prediction counter next to "Predict". Sized down from the link's own font
   so it reads as an annotation rather than a second label, and given a fixed
   `em` gap on the left because the nav link has no spacing of its own between
   text and what follows it. Tabular figures keep "3/18" and "13/18" the same
   width, so the menu does not twitch as the round fills up. */
.nav-badge {
  margin-left: 0.4em;
  font-size: 0.75em;
  font-variant-numeric: tabular-nums;
  vertical-align: 0.05em;
}

/* The unread dot on the collapsed menu button.

   The menu is shut on a phone, which is where this is read, so a badge that only
   lives inside it is a badge nobody sees. The toggler is Bootstrap's hamburger and
   has no text to hang a pill off, so the dot is drawn with ::after on the button
   itself - hence position: relative here, which the button does not have of its own.

   No number on it: a digit on a 40px button is unreadable, and the count is one tap
   away inside. It cannot double-render, because Bootstrap hides the toggler from lg
   up, which is exactly where the real pill becomes visible instead.

   The colour is the Predict badge's unfinished state, so "there is something you have
   not dealt with" looks the same wherever it appears. The dot is decorative and says
   nothing to a screen reader, which is why the toggler's aria-label carries the
   unread count in words instead - see _Layout.cshtml. */
.navbar-toggler.has-unread {
  position: relative;
}

.navbar-toggler.has-unread::after {
  content: "";
  position: absolute;
  top: 0.15rem;
  right: 0.15rem;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background-color: var(--bs-warning);
}

/* An update note's body is plain text, stored and rendered as typed: pre-line is what
   keeps the paragraph breaks without a single character of markup being interpreted. */
.update-note__body {
  white-space: pre-line;
}

/* Deliberately not .nav-icon, which is display: none above the lg breakpoint: the menu
   icons are a phone-only affordance and this thumbs up is the button's whole label. Same
   em sizing and optical nudge, so it sits on the text the way the menu ones do. */
.update-note__like-icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.125em;
}

/* The like button's count sits after the icon, and only when there is one. The gap is
   on the span rather than the icon so an empty count adds no trailing space. */
.update-note__count:not(:empty) {
  margin-left: 0.35em;
  font-variant-numeric: tabular-nums;
}

/* The season progress bar: a ball on the leading edge of the fill, and a goal standing
   clear of the track in a gutter of its own. For every value but the last the ball's
   centre is exactly the fill's edge, so it never runs ahead of the blue; the final match
   is the one that moves it off the track and into the net. --season-progress is the
   fraction played and .season-progress--complete marks the season finished, both set on
   the element by the page. */
.season-progress {
  --goal-width: 2.15rem;
  --goal-gutter: 2.4rem;        /* the track stops here; the goal stands in it */
  --ball-size: 1.05rem;

  position: relative;
  display: flex;
  align-items: center;
  height: 1.5rem;
  padding-right: var(--goal-gutter);
}

.season-progress__track {
  flex: 1 1 auto;
  height: 0.75rem;
}

/* Vertically centred on the track, not on the row, so the ball sits on the bar. */
.season-progress__goal,
.season-progress__ball {
  position: absolute;
  display: block;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
}

.season-progress__goal {
  right: calc((var(--goal-gutter) - var(--goal-width)) / 2);
  width: var(--goal-width);
  height: 1.45rem;
  z-index: 1;
}

/* The ball's centre sits on the fill's edge, so its travel is the track's width and not
   the wrapper's. The max() is the one exception: with nothing played the ball would hang
   half off the left edge, so it is held flush until the fill has grown past its radius. */
.season-progress__ball {
  left: calc(
    max((100% - var(--goal-gutter)) * var(--season-progress, 0), var(--ball-size) / 2)
    - var(--ball-size) / 2
  );
  width: var(--ball-size);
  height: var(--ball-size);
  z-index: 2;
}

/* Every match played: the ball leaves the track and finishes in the goal's mouth. */
.season-progress--complete .season-progress__ball {
  left: calc(100% - var(--goal-gutter) / 2 - var(--ball-size) / 2);
}

/* The unsaved-changes dialog stacks its three buttons on a phone and lays them out in
   a row from md up. Bootstrap's 500px dialog is a few pixels short of holding that row,
   which drops "Stay on this page" onto a line of its own and reads like an accident. */
@media (min-width: 768px) {
  #unsaved-modal .modal-dialog {
    max-width: 34rem;
  }
}

/* The save button on Predict follows the page once there is something to save.

   A round is eighteen matches, so the button that finishes the job sits two phone
   screens below the fold - and it is the one control the page exists for.

   position: sticky, not fixed: the button stays in the document flow, so at the foot
   of the page it parks in its natural place above the footer rather than floating over
   it, and with JavaScript off it is exactly the button it always was.

   The stuck state is switched on by the first keystroke, by the unsaved guard that is
   already tracking the dirty flag. A bar that is there from the start would eat a
   sixth of a phone viewport for a button with nothing to do, and would be tuned out
   by the time it mattered; appearing on the first change reads as an answer to it. */
.predict-save--stuck {
  position: sticky;
  bottom: 0;
  z-index: 3;
  padding-top: 0.75rem;
  padding-bottom: 0.75rem;
  /* Bleed out to the container's gutters, so no strip of scrolling content shows
     down either side of the bar. */
  margin-inline: calc(var(--bs-gutter-x, 1.5rem) * -0.5);
  padding-inline: calc(var(--bs-gutter-x, 1.5rem) * 0.5);
  background-color: var(--bs-body-bg);
  border-top: 1px solid var(--app-table-rule);
}

/* Matchday's two summary rows. The grid's own cells are painted red/amber/green, so the
   footer has to read as a different kind of row or it looks like one more prediction:
   a top rule and no verdict colour at all. */
.matchday-totals th,
.matchday-totals td {
  border-top: 2px solid var(--bs-border-color);
}

/* The running total sits in its own right-hand column on a phone, under the header word
   that names it, so a bold day figure and a muted total never read as one number. */
.matchday-total-col {
  min-width: 2.5rem;
  text-align: right;
}

/* Matchday's two layouts. The breakpoint still decides on its own - that is what a
   reader with no script, or with no stored choice, gets - and the data-view attribute
   overrides it in either direction. Two classes deep beats the media query's one, so
   the override needs no !important. */
.matchday-grid {
  display: none;
}

.matchday-cards {
  display: block;
}

@media (min-width: 768px) {
  .matchday-grid {
    display: block;
  }

  .matchday-cards {
    display: none;
  }
}

.matchday-views[data-view="grid"] .matchday-grid,
.matchday-views[data-view="cards"] .matchday-cards {
  display: block;
}

.matchday-views[data-view="grid"] .matchday-cards,
.matchday-views[data-view="cards"] .matchday-grid {
  display: none;
}

/* The grid on a phone. A row of eighteen scores says nothing without the fixture it
   belongs to, so the Match and Result columns pin to the left and everything else
   scrolls past them - the frozen block of the spreadsheet this game came from. Its two
   columns are 7.5rem + 3.25rem, and that ceiling is the whole design: much past 200px
   and there is no screen left to scroll anything into. */
@media (max-width: 767.98px) {
  .matchday-grid table {
    font-size: 0.8rem;
  }

  .matchday-grid th,
  .matchday-grid td {
    position: relative;
  }

  .matchday-grid thead th:first-child,
  .matchday-grid tbody td:first-child,
  .matchday-grid tfoot th:first-child,
  .matchday-grid thead th:nth-child(2),
  .matchday-grid tbody td:nth-child(2) {
    position: sticky;
    z-index: 2;
    /* Sticky needs something opaque under it: .table-transparent leaves these cells
       see-through so the scroll shadows show, which would let the columns being
       scrolled read straight through the pinned ones. */
    background-color: var(--bs-body-bg);
  }

  .matchday-grid thead th:first-child,
  .matchday-grid tbody td:first-child,
  .matchday-grid tfoot th:first-child {
    left: 0;
  }

  .matchday-grid thead th:nth-child(2),
  .matchday-grid tbody td:nth-child(2) {
    left: 7.5rem;
  }

  /* A sticky column has to be a known width - the second one is positioned at the first
     one's edge, and there is no way to ask CSS how wide the content made it. So the
     Match column is told what it is here, which is also why the nowrap + max-content
     rule above has to be undone: that rule exists to widen an auto-layout column until
     the longest name fits, and this column is not allowed to grow. Ellipsis instead,
     with the crest still naming the team. */
  .matchday-grid thead th:first-child,
  .matchday-grid tbody td:first-child {
    width: 7.5rem;
    min-width: 7.5rem;
    max-width: 7.5rem;
  }

  .matchday-grid td .match-stack__name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .matchday-grid td .match-stack__team {
    grid-template-columns: var(--match-crest-size) minmax(0, 1fr);
  }

  .matchday-grid td .match-stack__teams {
    padding-inline-end: 0.4rem;
  }

  .matchday-grid td:has(> .match-stack__teams) + td {
    padding-inline-start: 0.4rem;
  }

  .matchday-grid thead th:nth-child(2),
  .matchday-grid tbody td:nth-child(2) {
    width: 3.25rem;
    min-width: 3.25rem;
  }
}

/* The brackets are the first thing to go when the grid is asked for on a phone: "2:1 (3)"
   wraps to two lines in a 39px column, which costs a third of the rows' height and buys
   nothing the colour is not already saying. The legend's own mention of them goes at the
   same time, so the page never names a notation it is not using. The cards keep both -
   they have the room, and they are still the default here. */
@media (max-width: 767.98px) {
  .matchday-views[data-view="grid"] .cell-points {
    display: none;
  }
}

/* The cards are disclosures. Bootstrap's card markup sits on a <details>, so a shut card
   is one line and the day is one screen, with no script involved - the same bargain the
   home page's admin block struck. The browser's own marker would take a line of its own
   above the fixture, because the summary's contents are a block-level row, so the marker
   is replaced by a caret drawn in the row itself. Replaced, not removed: a card that is
   openable has to look openable. */
.matchday-cards > details > summary {
  display: flex;
  /* Top, not centre. A shut match card is now several lines tall - two teams, and a
     scorer line per side - and a caret centred on all of that floats away from the row
     it opens. The offset lines it up with the middle of the first line instead. */
  align-items: flex-start;
  gap: 0.5rem;
  cursor: pointer;
  list-style: none;
}

.matchday-cards > details > summary::-webkit-details-marker {
  display: none;
}

.matchday-cards > details > summary::before {
  content: "";
  flex: 0 0 auto;
  margin-top: 0.5rem;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid var(--bs-secondary-color);
  border-bottom: 2px solid var(--bs-secondary-color);
  transform: rotate(-45deg);
}

.matchday-cards > details[open] > summary::before {
  transform: rotate(45deg);
}

.matchday-cards > details > summary > div {
  flex: 1 1 auto;
  min-width: 0;
}

/* Swipe paging between pages. The attribute is stamped by _Layout only when the release
   gate is open and the person has not switched the feature off, so an ungated page never
   matches any of this. See the block at the end of site.js for the gesture itself. */

[data-swipe-track] {
  /* The transform is set from script on every touchmove, so the compositor is told in
     advance. No transition here on purpose: during the drag the track has to sit exactly
     where the finger is, and a transition would make it lag behind by its own duration. */
  will-change: transform;
}

/* The page being swiped to, fetched and held just off the screen until a finger asks for
   it. Built in site.js; everything here is only the box it goes in.

   Fixed and clipped, from the top of the content down: the header stays put while the
   content slides under it, which is the whole reason the layer does not carry a header of
   its own. z-index sits above the page and below Bootstrap's modals (1055), because a
   dialog is a question and a preview must never cover one. */
.swipe-preview {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  top: 0;
  z-index: 1000;
  overflow: hidden;
  visibility: hidden;
  will-change: transform;
}

/* Only while a finger is actually asking for it. Kept out of the way with visibility
   rather than display, so the layer keeps its size and does not have to be laid out again
   at the moment the drag starts. */
.swipe-preview.swipe-preview--showing {
  visibility: visible;
}

.swipe-preview.swipe-preview--settling {
  transition: transform 0.2s ease-out;
}

/* Animate the change from one page to the next, rather than blinking to it: the browser's
   own cross-document transition, no script and no library, and a browser without it -
   anything before iOS 18 - simply navigates the way it always did. A swipe that has
   already dragged the next page in under the finger asks in site.js for this one to be
   skipped, which leaves menu taps, form redirects and the back button.

   STILL COMMENTED OUT, and the reason is now only half known. Cache-Control: no-store was one
   cause and is gone - the middleware in Program.cs takes it off GET pages, which is what let the
   back/forward cache in and was worth doing on its own. It was not the only cause: with the
   header fixed, Edge 152 still never offers a transition on any page of this app, on a plain
   menu click, at any page length, while two static pages under wwwroot linking this very
   stylesheet transition on the first try. So it is something these documents do, not the rule
   and not the file.

   Left here rather than deleted because it is one uncomment away, and the next person to look
   has the bisect already done: static works, app does not, header ruled out, page height ruled
   out. Chasing it further was not worth the evening. See TODO.md. */
/* @view-transition {
  navigation: auto;
} */

/* @media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
} */

/* Only for letting go - the spring back when a swipe did not go far enough, and the last
   step of one that did. Added for the length of the animation and taken off again, so the
   next drag starts with no transition in the way. */
[data-swipe-track].swipe-track--settling {
  transition: transform 0.2s ease-out;
}

/* A track pushed a third of the screen aside would otherwise widen the page and give the
   whole document a horizontal scrollbar, which on a phone means the page can be dragged
   sideways after the gesture has finished. Scoped with :has so it only applies on the
   pages that actually have a track; where :has is missing the worst case is that scrollbar
   back, not a broken page. */
body:has([data-swipe-track]) {
  overflow-x: hidden;
}

/* The previous and next round buttons beside a round picker, built in site.js. Not gated
   and not switchable, unlike the page swipe above: a pair of buttons next to a picker
   collides with nothing. Square rather than button-shaped, because they sit either side of
   a select in a flex row and a wide button would push the select itself down to nothing on
   a narrow phone. 2.5rem is above the 44px the platforms ask of a touch target. */
.round-arrow {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  min-height: 2.5rem;
  padding: 0;
}

/* Disabled at the ends of the list rather than hidden, so nothing moves under a thumb
   already on its way down. Faded further than Bootstrap's own disabled state because these
   carry no text to read - the only thing saying "not now" is the contrast. */
.round-arrow:disabled {
  opacity: 0.3;
}

/* The leaderboard on a phone.

   Seven columns and every Estonian header a compound with no break opportunity in it, so
   the column's minimum is the whole word - Mängupunktid alone asked for 105px - and the
   table demanded 515px inside the 339px a 360px screen has. That is what made
   .table-responsive scroll sideways in Estonian and never in English, where "Match
   points" splits over two lines by itself.

   Shorter words did most of the work (see AppResources.et.resx) but not enough: the short
   set still wanted 374px. The rest is here. Headers a size down, and a fifth of a rem of
   padding either side of eight columns, bring the minimum to 324px - which fits from
   360px up with the prize column showing and a thirteen-character name in it. 320px is
   still 25px over and falls back to scrolling, which is why the wrapper stays.

   Letting the name wrap as insurance was tried and is wrong: overflow-wrap on that cell
   makes the name the column auto layout squeezes, and "Richard" came out "Rich/ard" at
   412px where there was room to spare - the trap td .match-stack__name documents above. */
@media (max-width: 767.98px) {
  .leaderboard-table th {
    font-size: 0.8125rem;
  }

  .leaderboard-table.table-sm > :not(caption) > * > * {
    padding-inline: 0.15rem;
  }
}

/* Players page: the number headings are buttons that reorder the table, drawn as plain
   heading text. The one the table is ordered by is underlined, with an arrow for which
   way round. */
.players-page__sort {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: inherit;
}

.players-page__sort[data-direction] {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.players-page__sort[data-direction="descending"]::after {
  content: " \25BE";
}

.players-page__sort[data-direction="ascending"]::after {
  content: " \25B4";
}

.players-page__sort .red-card,
.players-page__sort .yellow-card {
  margin-inline-start: 0;
}

/* The Live page: tonight's matches as a grid of tiles, and the leaderboard under them.

   Two tiles to a phone row and as many as fit on a desktop. A match night is nine of
   them, so at one per row the leaderboard - which is what the page is opened for - would
   start below the fold on every phone there is. */
.live-matches {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: 0.5rem;
}

.live-match {
  margin-bottom: 0;
}

/* The whole tile is the summary, so the disclosure triangle would sit alone above three
   lines of scoreboard. Taken away, and the tile is made to look pressable instead. */
.live-match > summary {
  list-style: none;
  cursor: pointer;
  padding: 0.4rem 0.5rem;
}

.live-match > summary::-webkit-details-marker {
  display: none;
}

.live-match__status {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-height: 1.5rem;
  margin-bottom: 0.15rem;
}

/* Crest, code, score - three tracks, and the score pinned to the right edge so that the
   column of scores lines up down the whole grid however long the codes are. */
.live-match__side {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.35rem;
  line-height: 1.5;
}

.live-match__code {
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.live-match__score {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.live-match__goals {
  padding: 0.4rem 0.5rem;
  border-top: 1px solid var(--app-table-rule);
}

/* The feed under the tiles. Five lines of room and the rest behind a scroll: an evening
   of four matches is thirty bookings and goals, and a block that grew to that height
   would push the leaderboard off the phone. Newest first, so the scroll position lost on
   every refresh is a position nobody wanted to keep. */
.live-feed {
  margin-top: 1rem;
}

.live-feed__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  cursor: pointer;
}

.live-feed__list {
  max-height: 11.5rem;
  overflow-y: auto;
}

.live-feed__item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.5rem;
}

/* The minute is the column the eye runs down, so it keeps its width whether it says 8'
   or 90+4'. */
.live-feed__minute {
  flex: 0 0 2.9rem;
  font-variant-numeric: tabular-nums;
}

/* Minute, marker, crest and code are all fixed columns, so the names start at the same
   place down the whole list. A ball is wider than a card and "CRY" is wider than "LEE",
   and left to themselves each line would begin a few pixels off from the one above it. */
.live-feed__marker {
  display: flex;
  flex: 0 0 1.4em;
  align-items: center;
  justify-content: center;
}

.live-feed__item .team-crest {
  width: 1.25em;
  height: 1.25em;
}

.live-feed__team {
  flex: 0 0 2.6rem;
  font-weight: 600;
}

/* Only the player's name gives way, and it gives way by cutting rather than by wrapping:
   a two-line entry in a five-line box costs a fifth of the block. */
.live-feed__player {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.live-feed__score {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}

.live-feed__item .red-card,
.live-feed__item .yellow-card {
  flex: 0 0 auto;
  margin-inline-start: 0;
}

/* Sized against the cards beside it rather than against the text: the three markers make
   a column, and a ball that matched the line height would tower over two rectangles that
   do not. */
.live-feed__ball {
  display: block;
  flex: 0 0 auto;
  width: 1.25em;
  height: 1.25em;
}

/* Position, name, arrow, tonight, total - and only the name is allowed to give way, so
   the four numbers stay in their columns down the whole list. */
.live-standing > summary {
  display: grid;
  grid-template-columns: 1.75rem 1fr auto 2.5rem 2.75rem;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 0.5rem;
  list-style: none;
  cursor: pointer;
  border-bottom: 1px solid var(--app-table-rule);
}

.live-standing > summary::-webkit-details-marker {
  display: none;
}

.live-standing__position {
  font-variant-numeric: tabular-nums;
  color: var(--bs-secondary-color);
  text-align: end;
}

.live-standing__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.live-standing__day,
.live-standing__total {
  font-variant-numeric: tabular-nums;
  text-align: end;
}

.live-day--scored {
  color: var(--app-cell-exact-border);
  font-weight: 600;
}

.live-move--up {
  color: var(--app-cell-exact-border);
}

.live-move--down {
  color: var(--app-cell-wrong-border);
}

/* Bottom of the screen and out of the thumb's way, not under it. Fixed rather than in
   flow: a goal arrives while the reader is somewhere down the leaderboard, and a message
   that appeared at the top of the page would announce itself to nobody. */
.live-toasts {
  position: fixed;
  inset-block-end: 1rem;
  inset-inline-end: 1rem;
  z-index: 1080;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  max-width: min(22rem, calc(100vw - 2rem));
  pointer-events: none;
}

.live-toast {
  display: flex;
  flex-direction: column;
  padding: 0.5rem 0.75rem;
  border-radius: 0.5rem;
  border-inline-start: 4px solid var(--app-accent);
  background-color: var(--app-nav-bg);
  box-shadow: 0 2px 10px var(--app-scroll-shadow);
  font-size: 0.875rem;
  animation: live-toast-in 160ms ease-out;
}

@keyframes live-toast-in {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .live-toast {
    animation: none;
  }
}

/* The reader's own line on a tile: prediction on the left, what it is worth on the
   right, separated by the free space rather than by a rule. */
.live-match__you {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.35rem;
  margin-top: 0.25rem;
  padding-top: 0.25rem;
  border-top: 1px solid var(--app-table-rule);
}

/* The badge on a match being played. Its word is the same key as the Live page's
   title, so the shouting is done here rather than by a second resource string. */
.live-badge {
  text-transform: uppercase;
}
