/*
 * The fixtures filter, as a bottom sheet.
 *
 * Constrained to the shell width rather than the window, same as the search
 * overlay and the story viewer: on a phone that is genuinely full-bleed, and on
 * desktop it keeps the mini app inside its own frame instead of dimming the
 * browser tab around it.
 *
 * Two layers that stay apart, because they animate different properties on
 * different schedules — which is the whole of the entry choreography:
 *
 *   .filter-sheet__scrim   the darkening. Fades on --duration with --easing,
 *                          starting immediately.
 *   .filter-sheet__panel   the travel. Springs up one --stagger-step later, so
 *                          the room dims a beat before the sheet arrives rather
 *                          than the two moving in lockstep.
 *
 * That 40ms is the shared --stagger-step, not a number of this file's own: it
 * is the same beat the section cascades in the search overlay and the story
 * rings are spaced by, which is what makes the offset read as the app's timing
 * rather than as this sheet's.
 */

.filter-sheet {
  /* Where the panel rests when closed — its own full height, below the fold. */
  --rest-y: 100%;

  position: fixed;
  inset: 0;
  /*
   * Above the search overlay's 90 and below the story viewer's 100. The three
   * never coexist — this sheet's scrim covers the bar both of the others are
   * opened from — but the order is the order they would have to stack in.
   */
  z-index: 95;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  max-width: var(--app-max-width);
  margin-inline: auto;
}

/* A class beats the UA [hidden] rule, so display has to be turned off by hand. */
.filter-sheet[hidden] {
  display: none;
}

/*
 * Opaque fill, animated opacity — the story viewer's own backdrop, and for the
 * same reason: --surface-inverse is the app's one dark ground, and fading an
 * element's opacity composites on the GPU where fading a colour does not.
 */
.filter-sheet__scrim {
  position: absolute;
  inset: 0;
  background: var(--surface-inverse);
  opacity: 0;
  transition: opacity var(--duration) var(--easing);
  /*
   * On the way out the darkening waits a beat and the panel leaves first —
   * the entry read backwards, so the sheet is never gone from a screen that is
   * still dark for no reason.
   */
  transition-delay: var(--stagger-step);
}

.filter-sheet[data-open="true"] .filter-sheet__scrim {
  opacity: 0.45;
  transition-delay: 0ms;
}

/* --- Panel ---------------------------------------------------------------- */

.filter-sheet__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  /*
   * Tall enough to be worth opening, short enough to leave the list visible
   * above it — a filter you cannot see the effect of is a form, not a preview.
   * dvh rather than vh: Telegram's WebView resizes its own chrome, and vh would
   * measure a viewport the sheet is not actually in.
   */
  max-height: min(86dvh, 720px);
  background: var(--surface);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: var(--shadow-floating);
  /*
   * Entry and dismiss on one property. The drag writes --drag-y directly and
   * the resting offset is a token the open state zeroes, so a dismiss that
   * starts mid-entry adds to the rise instead of fighting it for `transform`.
   */
  transform: translateY(calc(var(--rest-y) + var(--drag-y, 0px)));
  /*
   * --exit-duration is written by the dismiss commit, from the distance still
   * to travel at the speed the finger was already moving. Unset — a close from
   * the backdrop, the footer or the back button — it falls back to the shared
   * fade duration.
   */
  transition: transform var(--exit-duration, var(--duration)) var(--easing);
}

/*
 * A sheet is an object arriving, which is what INTERACTIONS.md files a spring
 * for. The delay is the beat behind the scrim; on the way out there is none,
 * because the panel is the thing that should leave first.
 */
.filter-sheet[data-open="true"] .filter-sheet__panel {
  --rest-y: 0%;

  transition: transform var(--spring-duration) var(--spring) var(--stagger-step);
}

/*
 * Both dragging states have to come after the open rule: they share its
 * specificity and only override the transition, so source order is what decides
 * which wins while the sheet is open.
 *
 * Under the finger nothing eases — the sheet is being held, not animated.
 */
.filter-sheet[data-dragging="true"] .filter-sheet__panel {
  transition: none;
}

/*
 * A drag that ended where it started springs home, which is exactly what an
 * overshoot is for. Its own state rather than the resting "false": the entry
 * rests in the same place and springs *behind the scrim's beat*, and a
 * snap-back has no beat to wait for.
 */
.filter-sheet[data-dragging="released"] .filter-sheet__panel {
  transition: transform var(--spring-duration) var(--spring);
}

/* --- Grabber -------------------------------------------------------------- */

/*
 * A real handle. It owns the dismiss drag together with the head below it,
 * which is why the grab cursor is honest here and why `touch-action: none` is
 * load-bearing: without it the WebView decides this touch belongs to the
 * scrolling body underneath and cancels the pointer stream mid-gesture.
 *
 * Selection is off for the same reason it is off on the search overlay's
 * header — left on, the browser starts a native selection drag and fires
 * pointercancel mid-swipe, and on iOS a long press raises the callout menu
 * instead of reaching the handler.
 */
.filter-sheet__grabber {
  flex: none;
  display: grid;
  place-items: center;
  padding: var(--space-2) 0 var(--space-1);
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.filter-sheet__grabber:active {
  cursor: grabbing;
}

.filter-sheet__grabber-bar {
  width: 36px;
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--surface-pressed);
}

/* --- Head ----------------------------------------------------------------- */

.filter-sheet__head {
  flex: none;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-1) var(--space-4) var(--space-3);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.filter-sheet__identity {
  flex: 1;
  min-width: 0;
}

.filter-sheet__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
}

/*
 * Both states occupy one grid cell so they cross-fade in place. Two stacked
 * spans rather than one element whose text is swapped: a hard cut on a line
 * that never moves reads as a glitch, and there is no way to fade text against
 * itself in a single node.
 */
.filter-sheet__subtitle {
  display: grid;
}

.filter-sheet__subtitle-text {
  grid-area: 1 / 1;
  color: var(--text-muted);
  font-size: var(--text-xs);
  opacity: 0;
  transition: opacity var(--duration) var(--easing);
}

.filter-sheet__subtitle-text[data-shown="true"] {
  opacity: 1;
}

/*
 * Bare accent type, not a button box: it sits in the same row as the title and
 * a second card there would make the head two things instead of one. The
 * confirmed state drops to muted — the label has already changed to "Cleared",
 * and leaving it in the accent would keep offering an action that just ran.
 */
.filter-reset {
  flex: none;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-full);
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  transition: color var(--duration) var(--easing);
}

.filter-reset[data-confirmed] {
  color: var(--text-muted);
}

/* --- Tabs ----------------------------------------------------------------- */

/*
 * The search overlay's tab row, redrawn for this sheet. Deliberately not a new
 * treatment: two tabbed surfaces in one app that disagree about what a tab
 * looks like is the drift the token files exist to prevent.
 */
.filter-tabs {
  position: relative;
  flex: none;
  display: flex;
  padding-inline: var(--space-4);
  border-bottom: 1px solid var(--border);
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.filter-tab {
  /* Zero basis, so the row splits evenly rather than by label length. */
  flex: 1 1 0;
  min-width: 0;
  padding: var(--space-2) var(--space-1) var(--space-3);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  transition: color var(--duration) var(--easing);
}

.filter-tab[aria-selected="true"] {
  color: var(--accent);
}

.filter-tabs__indicator {
  position: absolute;
  bottom: -1px;
  left: 0;
  height: 2px;
  border-radius: var(--radius-full);
  background: var(--accent);
  opacity: 0;
  transform-origin: left center;
  pointer-events: none;
}

/* Added one frame after the first placement so it does not fly in from 0. */
.filter-tabs--ready .filter-tabs__indicator {
  opacity: 1;
  transition:
    transform var(--spring-duration) var(--spring),
    opacity var(--duration) var(--easing);
}

/* --- Body ----------------------------------------------------------------- */

.filter-sheet__body {
  position: relative;
  flex: 1;
  overflow-y: auto;
  /* The page behind is already scroll-locked; this stops a flick at either end
     of the panel handing the gesture back to it anyway. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--space-4);
}

/*
 * The tab switch, eased.
 *
 * Two tabs of different lengths make the sheet a different size, and cutting
 * between the two heights is what makes a switch read as blocky however nicely
 * the contents themselves animate. So the box travels: `height` on this one
 * element, for one --duration-slow, landing on a pose it must not pass — hence
 * --easing rather than the spring.
 *
 * `flex: none` is not optional. The resting `flex: 1` is `1 1 0%`, and a flex
 * item with a zero basis takes its main size from the flex distribution rather
 * than from `height` — left alone, the explicit height would be ignored and
 * nothing would move at all.
 *
 * Scrolling is off for the duration: a box mid-resize has a scroll height that
 * is about to be wrong, and a scrollbar flashing in and back out of a sheet is
 * the artefact this whole transition exists to avoid.
 */
.filter-sheet__body[data-resizing] {
  flex: none;
  overflow: hidden;
  transition: height var(--duration-slow) var(--easing);
}

.filter-panel[hidden] {
  display: none;
}

.filter-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/*
 * The outgoing panel, on its way out. Taken out of flow in the same frame the
 * switch starts — the box behind it has to be free to begin easing towards the
 * incoming panel's height immediately — and fading from where it stood rather
 * than being switched off in one cut.
 *
 * The insets repeat the body's own padding on purpose: an absolutely positioned
 * child resolves against its containing block's *padding box*, so `0` here
 * would put the panel against the scroller's edge rather than where it has been
 * sitting, and the fade would start with a jump sideways.
 */
.filter-panel--leaving {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  left: var(--space-4);
  pointer-events: none;
  animation: filter-panel-out var(--duration) var(--easing) forwards;
}

@keyframes filter-panel-out {
  to {
    opacity: 0;
    transform: translateY(-6px);
  }
}

.filter-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/*
 * The cascade, one --stagger-step per group. Scoped to a class the JS removes
 * and re-adds rather than living on .filter-group directly: a delay-based
 * entrance only plays when the animation restarts, and the panels persist
 * across tab switches — so on `.filter-group` alone the second tab would arrive
 * already finished, every time after the first.
 */
.filter-panel--entering .filter-group {
  animation: filter-group-in var(--duration) var(--easing) backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger-step));
}

@keyframes filter-group-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

.filter-group__title {
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  letter-spacing: -0.01em;
}

.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* --- Chips ---------------------------------------------------------------- */

/*
 * Darker fill inside a grey card, which is exactly what --surface-sunken is for,
 * and the filled accent on selection — the same "this one is active" signal the
 * date strip and the bottom nav use. Never an outline: on a light surface that
 * reads as a focus state.
 *
 * **This is no longer only the filter sheet's.** The fixture page's Head to
 * Head tab mounts two of these to filter its meetings, unchanged and
 * unscoped — a chip that means "narrow what is below me" should not look
 * like two different controls on two surfaces. Nothing in this block may
 * grow a `.filter-sheet` ancestor in its selector without first moving the
 * block somewhere both surfaces can reach.
 */
.filter-chip {
  display: inline-flex;
  align-items: center;
  /*
   * 6px rather than --space-1. The picker chips carry a flag or a crest in
   * front of the name, and 4px sets an 18px mark hard against 13px type;
   * --space-2 the other way opens a gap before the fixture count that makes
   * every chip in a hundred-chip cloud wider than it needs to be. The league
   * strip's own track takes the same off-scale 6px, for the same reason.
   */
  gap: 6px;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-sunken);
  border-radius: var(--radius-full);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  transition:
    background-color var(--duration) var(--easing),
    color var(--duration) var(--easing);
}

.filter-chip[aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-text);
  font-weight: var(--weight-semibold);
}

/*
 * The Live chip, at rest. This is `.pill--live` exactly — no fill, the colour
 * on the type, bold — because that is already how this app says LIVE, and the
 * chip is naming the same fact the running minute in a match row does.
 *
 * Selected, it takes the ordinary accent fill like every other chip. The red is
 * a foreground colour here by design (see --highlight in tokens.css: there is
 * deliberately no companion token for type set *on* it), so a red-filled chip
 * with a label on it is the one thing this must not become.
 */
.filter-chip--live:not([aria-pressed="true"]) {
  color: var(--highlight);
  font-weight: var(--weight-bold);
}

/*
 * The flag or crest in front of a picker chip's name.
 *
 * White behind the mark in both chip states, which .logo does not give it:
 * competition crests are transparent PNGs, and on the filled-accent chip a bare
 * one would sit on blue with nothing to read against. Framing it is what the
 * search overlay's badge disc already does for a crest anywhere else.
 */
.filter-chip .logo {
  background: var(--surface);
  /*
   * Inset rather than a border: `box-sizing: border-box` is global, so a real
   * border would eat into the 18px box and shrink the mark inside it.
   *
   * The hairline is what gives a flag an edge. Several of them are a device on
   * white — England's cross, Finland's, the Nordic flags generally — and cropped
   * to a disc on a white chip they would otherwise read as a mark floating in
   * the middle of nothing, with no sense of being a flag at all. It frames the
   * crests to the same effect, which is what .search-card__media already does
   * everywhere else a badge appears.
   */
  box-shadow: inset 0 0 0 1px var(--border);
}

/*
 * The monogram fallback, at chip size. --text-xs in an 18px box puts two
 * letters hard against the edges; 9px is the size the nav badge already sets
 * two characters at, and it is the same job.
 */
.filter-chip .logo--monogram {
  font-size: 9px;
  letter-spacing: 0;
}

.filter-chip__count {
  color: var(--text-muted);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
  transition: color var(--duration) var(--easing);
}

.filter-chip[aria-pressed="true"] .filter-chip__count {
  color: rgb(var(--text-inverse-rgb) / 0.72);
}

/*
 * The selection overshoot. --easing rather than --spring: the keyframe already
 * carries the overshoot, and layering the spring on top would take the chip
 * past 1.05 on the way up — the same call the date card's pop makes.
 *
 * --press is multiplied into every frame rather than the animation simply
 * replacing the transform. An animation outranks the base rule outright
 * (INTERACTIONS.md says so in as many words), so a chip pressed again during
 * its own pop would otherwise ignore the touch entirely.
 */
.filter-chip[data-pop="true"] {
  animation: filter-chip-pop var(--spring-duration) var(--easing);
}

@keyframes filter-chip-pop {
  0% {
    transform: scale(calc(1 * var(--press, 1)));
  }
  45% {
    transform: scale(calc(1.05 * var(--press, 1)));
  }
  100% {
    transform: scale(calc(1 * var(--press, 1)));
  }
}

/* --- Custom time window --------------------------------------------------- */

/*
 * 0fr -> 1fr gives the animated open without ever measuring a height, and
 * without the max-height guess that either clips the row or coasts through
 * empty space. The app's own collapse — see LeagueAccordion.
 */
.filter-custom {
  display: grid;
  grid-template-rows: 0fr;
  /*
   * Collapsed, the panel is zero-height but its parent's flex `gap` is not —
   * a 12px band of nothing would sit under the chips whenever Custom is
   * unselected, which is most of the time. The negative margin cancels exactly
   * that gap, and travels with the row so the reveal stays one movement instead
   * of a snap followed by a slide.
   */
  margin-top: calc(-1 * var(--space-3));
  transition:
    grid-template-rows var(--duration) var(--easing),
    margin-top var(--duration) var(--easing);
}

.filter-custom[data-shown="true"] {
  grid-template-rows: 1fr;
  margin-top: 0;
}

.filter-custom__inner {
  min-height: 0;
  overflow: hidden;
}

/*
 * The same card the search field wears, at field size: a transparent border
 * that the focus state colours, rather than a ring drawn outside the box. On a
 * bordered field base.css's offset outline paints a second rounded box a couple
 * of pixels off the first, which reads as a rendering fault.
 */
.filter-time {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  transition: border-color var(--duration) var(--easing);
}

.filter-time:focus-within {
  border-color: var(--accent);
}

.filter-time__label {
  color: var(--text-muted);
  font-size: var(--text-2xs);
  font-weight: var(--weight-medium);
}

/*
 * Safari gives a time input its own intrinsic width and a centred value box,
 * neither of which survives being one of two fields sharing a row — hence the
 * appearance reset and the explicit alignment.
 */
.filter-time__input {
  width: 100%;
  min-width: 0;
  border: 0;
  background: none;
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  -webkit-appearance: none;
  appearance: none;
}

.filter-time__input::-webkit-date-and-time-value {
  text-align: left;
  margin: 0;
}

.filter-time__input:focus-visible {
  outline: none;
}

/* --- Switch --------------------------------------------------------------- */

.filter-switch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3);
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
  text-align: left;
}

.filter-switch__text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.filter-switch__label {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}

.filter-switch__hint {
  color: var(--text-muted);
  font-size: var(--text-xs);
  line-height: 1.3;
}

.filter-switch__track {
  position: relative;
  flex: none;
  width: 44px;
  height: 26px;
  background: var(--surface-pressed);
  border-radius: var(--radius-full);
  transition: background-color var(--duration) var(--easing);
}

.filter-switch[aria-checked="true"] .filter-switch__track {
  background: var(--accent);
}

/* The knob is the one thing in the control that travels, so it is the one
   thing that springs. */
.filter-switch__knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  background: var(--surface);
  border-radius: var(--radius-full);
  box-shadow: var(--shadow-floating);
  transition: transform var(--spring-duration) var(--spring);
}

.filter-switch[aria-checked="true"] .filter-switch__knob {
  transform: translateX(18px);
}

/* --- Pickers -------------------------------------------------------------- */

.filter-cloud {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/*
 * The field that narrows the chips below it. Same card as the time inputs —
 * sunken fill inside a white sheet, with a transparent border the focus state
 * colours. base.css's offset outline would paint a second rounded box a couple
 * of pixels off this one, which reads as a rendering fault rather than as focus.
 */
.filter-search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  transition: border-color var(--duration) var(--easing);
}

.filter-search:focus-within {
  border-color: var(--accent);
}

.filter-search__icon {
  color: var(--text-muted);
}

.filter-search__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  /* The placeholder counts what it is searching, so it has to hold one line
     inside the sheet's own padding — --text-md overruns it. */
  font-size: var(--text-sm);
}

.filter-search__input::placeholder {
  color: var(--text-muted);
}

.filter-search__input:focus-visible {
  outline: none;
}

/*
 * One step deeper than the field it sits in, which is what --surface-pressed is
 * for: the field is already the sunken grey, so a clear button wearing the same
 * fill would be invisible inside it.
 */
.filter-search__clear {
  display: grid;
  place-items: center;
  flex: none;
  width: 22px;
  height: 22px;
  background: var(--surface-pressed);
  border-radius: var(--radius-full);
  color: var(--text-secondary);
}

/* .filter-search__clear sets `display`, which outranks the UA [hidden] rule. */
.filter-search__clear[hidden],
.filter-row[hidden] {
  display: none;
}

/*
 * Chips disclosed by an expander fade in where they land. Only the arrivals
 * carry this — the ten already on screen did not move, and re-announcing them
 * would make a disclosure look like a rebuild.
 */
.filter-chip[data-appeared="true"] {
  animation: filter-group-in var(--duration) var(--easing) backwards;
}

/*
 * The expander is a chip that is not an option — the soft accent tint keeps it
 * legible as a control among the values without borrowing the filled state that
 * means "selected".
 */
.filter-chip--more {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: var(--weight-semibold);
}

/*
 * Points right when closed and down when open — the fixtures accordion's own
 * scheme, not a 180deg flip. Two expandable things in one app that disagree
 * about which way a chevron turns is exactly the drift worth not paying for.
 */
.filter-chip__chevron {
  display: grid;
  place-items: center;
  transform: rotate(-90deg);
  transition: transform var(--duration) var(--easing);
}

.filter-chip--more[aria-expanded="true"] .filter-chip__chevron {
  transform: rotate(0deg);
}

.filter-empty {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* --- Footer --------------------------------------------------------------- */

/*
 * Pinned outside the scroller, and clearing whichever bottom obstruction is
 * larger — the device safe area, or the slice of the window Telegram's own
 * chrome is covering. The same calc .app__nav uses, for the same reason.
 */
.filter-sheet__footer {
  flex: none;
  padding: var(--space-3) var(--space-4)
    calc(var(--space-4) + max(var(--safe-bottom), var(--tg-chrome-inset, 0px)));
  border-top: 1px solid var(--border);
}

.filter-sheet__done {
  width: 100%;
  justify-content: center;
}

/*
 * Zero results is not an error and not a disabled control — the button still
 * closes, and the empty state behind it is what explains the number. It just
 * stops wearing the accent, which is reserved for something worth doing.
 */
.filter-sheet__done[data-empty="true"] {
  background: var(--surface-pressed);
  color: var(--text-muted);
  box-shadow: none;
}

/* --- Page behind ---------------------------------------------------------- */

/*
 * The page must not scroll under the sheet. Position is preserved on the
 * document, which is what lets the close land the user exactly where they were.
 */
body[data-filter-open] {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .filter-panel--entering .filter-group,
  .filter-panel--leaving,
  .filter-chip[data-appeared="true"],
  .filter-chip[data-pop="true"] {
    animation: none;
  }

  /* --duration-slow is already 0ms here, but the outgoing panel still has to
     stop occupying the box it faded out of. */
  .filter-panel--leaving {
    opacity: 0;
  }
}
