Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
/* stylelint-disable stylelint-plugin-mozilla/use-design-tokens */
.sports {
border-radius: var(--border-radius-large);
padding: var(--space-medium);
display: flex;
flex-direction: column;
box-shadow: var(--box-shadow-level-2);
height: 100%;
position: relative;
// End-of-match celebration tokens, used by the celebration that plays when a
// followed team's match ends.
--widget-celebration-accent-color: var(--color-accent-primary);
--widget-celebration-copy-color: var(--text-color);
--widget-celebration-surface-color: var(--background-color-box);
--widget-celebration-headline-gradient-angle: 90deg;
--widget-celebration-headline-gradient-start: var(--color-orange-20);
--widget-celebration-headline-gradient-mid-start: var(--color-orange-30);
--widget-celebration-headline-gradient-mid-end: var(--color-pink-30);
--widget-celebration-headline-gradient-end: var(--color-pink-40);
--widget-celebration-tint-color: transparent;
--widget-celebration-glow-color: color-mix(
in srgb,
var(--widget-celebration-accent-color) 35%,
transparent
);
--widget-celebration-orbit-color: color-mix(
in srgb,
var(--widget-celebration-accent-color) 70%,
transparent
);
--widget-celebration-track-color: color-mix(
in srgb,
var(--widget-celebration-accent-color) 45%,
transparent
);
--widget-celebration-border-draw-duration: 1900ms;
--widget-celebration-orbit-duration: 1900ms;
--widget-celebration-hold-duration: 3400ms;
--widget-celebration-exit-duration: 1200ms;
--widget-celebration-enter-ease: cubic-bezier(0.2, 0.9, 0.2, 1);
--widget-celebration-headline-delay: 80ms;
--widget-celebration-panel-delay: var(--widget-celebration-headline-delay);
--widget-celebration-illustration-delay: 180ms;
--widget-celebration-subhead-delay: 280ms;
@include widget-base-style;
@include widget-celebration-styles('sports-celebration');
.sports-celebration-illustration {
inset-inline: 0;
margin-inline: auto;
transform-origin: bottom center;
translate: -18% 0;
}
// Hide the "View all" / "Show less" toggle when the celebration starts, but
// bring it back on its own short timer rather than waiting for the full
// celebration lifecycle (hold + exit) to finish. visibility keeps its space
// so the card doesn't reflow. Tune --sports-view-all-return-delay to taste.
--sports-view-all-return-delay: 4000ms;
&.is-celebrating .sports-view-all {
animation: sports-celebration-restore-view-all
var(--sports-view-all-return-delay) steps(1, jump-end) forwards;
}
// Followed-team celebration: replace the generic animated stroke with a 2px
// border in the followed team's colors (a linear gradient supplied via
// --sports-celebration-border-gradient). Same gradient-border technique as
// .is-followed-highlight, faded in/out with the celebration.
&.is-followed-celebration {
.sports-celebration-stroke,
.sports-celebration-stroke-track,
.sports-celebration-stroke-orbit {
display: none;
}
// When the user follows the winning team, the persistent followed-highlight
// border is already on; hide it during the celebration so its 2px team
// gradient doesn't stack with the celebration's animated one (::after).
&.is-followed-highlight::before {
display: none;
}
&::after {
content: '';
position: absolute;
inset: 0;
z-index: 2;
border-radius: inherit;
padding: 2px; /* stylelint-disable-line declaration-property-value-disallowed-list */
background: var(--sports-celebration-border-gradient);
// Cuts a hole in the middle so only the 2px outer ring shows.
mask:
linear-gradient(#FFF 0 0) content-box,
linear-gradient(#FFF 0 0);
mask-composite: exclude;
opacity: 0;
pointer-events: none;
animation-name: sports-celebration-border-in, sports-celebration-border-out;
animation-duration: 260ms, var(--widget-celebration-exit-duration);
animation-timing-function: var(--widget-celebration-enter-ease), ease;
animation-fill-mode: forwards, forwards;
animation-delay: 0ms, var(--widget-celebration-hold-duration);
}
}
&:hover,
&:has(:focus-visible) {
outline: 1px solid var(--border-color-deemphasized);
box-shadow: var(--card-box-shadow-hover);
}
&.is-followed-highlight {
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px; /* stylelint-disable-line declaration-property-value-disallowed-list */
background: var(--sports-followed-gradient);
// Cuts a hole in the middle so only the 2px outer ring shows,
// turning the gradient fill into a gradient border.
mask:
linear-gradient(#FFF 0 0) content-box,
linear-gradient(#FFF 0 0);
mask-composite: exclude;
pointer-events: none;
}
}
&.medium-widget {
.sports-title-wrapper {
align-items: center;
margin-block-end: 0;
}
.sports-intro-wrapper > .sports-intro-lede {
margin: 0;
}
.sports-intro-title {
font-size: var(--font-size-root);
}
.sports-buttons-wrapper {
margin-block-start: var(--space-medium);
}
.sports-body {
// Bleed into the widget padding so the View all button focus outline isn't clipped.
margin-block-end: calc(-1 * var(--space-xsmall));
padding-block-end: var(--space-xsmall);
justify-content: space-between;
}
.match-highlight-view {
align-self: first baseline;
margin-block-end: auto;
}
// The medium card is shorter, so scale the celebration kit down to fit.
.sports-celebration-illustration {
block-size: 92px;
}
// Shorter fall distance on the shorter medium card: same (in-budget)
// duration over less distance reads as a slower drift, and the pieces
// still finish before the celebration unmounts.
.sports-celebration-confetti-piece {
--confetti-fall: 230px;
}
}
&.large-widget {
.sports-intro-wrapper {
margin-block-start: var(--space-large);
}
.sports-buttons-wrapper {
margin-block-start: var(--space-large);
}
}
&.sports-intro {
position: relative;
overflow: hidden;
.sports-intro-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: bottom center;
pointer-events: none;
}
> .sports-title-wrapper,
> .sports-body {
position: relative;
z-index: 1;
}
}
&.sports-follow-state {
.sports-title-wrapper {
padding: var(--space-small);
}
}
&.sports-matches {
.sports-title-wrapper {
grid-template-columns: auto 1fr auto;
}
}
&.sports-key-dates {
.sports-title-wrapper {
grid-template-columns: auto 1fr auto;
> h3 {
font-size: var(--font-size-small);
margin-block: 0;
justify-self: start;
align-self: center;
margin-inline-start: var(--space-small);
font-weight: var(--font-weight);
color: var(--text-color-deemphasized);
}
}
}
}
.sports-title-wrapper {
display: grid;
grid-template-columns: 1fr auto 1fr;
margin-block-end: var(--space-xsmall);
.sports-intro-title {
margin-block: 0;
max-width: 250px;
}
.sports-intro-lede {
margin-block-end: var(--space-small);
}
}
.sports-context-menu-wrapper {
grid-column: 3;
justify-self: end;
opacity: 0;
pointer-events: none;
}
.sports:hover,
.sports:has(:focus-visible) {
.sports-context-menu-wrapper {
opacity: 1;
pointer-events: auto;
}
}
// Handles keyboard focus on moz-button, since :has(:focus-visible) doesn't include shadow DOM
.sports-context-menu-wrapper:focus-within,
.sports-context-menu-wrapper:has(panel-list[open]) {
opacity: 1;
pointer-events: auto;
}
.sports-body {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-small);
overflow-y: auto;
scrollbar-width: none;
overflow-x: hidden;
padding-inline: var(--space-small);
padding-block: var(--space-xsmall) var(--space-small);
}
.sports-live-scores {
display: flex;
flex-direction: column;
gap: var(--space-xsmall);
}
.sports-intro-wrapper {
text-align: center;
}
.sports-intro-title {
margin-block: 0;
font-size: var(--font-size-heading-xlarge);
font-weight: var(--font-weight-heading);
}
.sports-buttons-wrapper {
display: flex;
flex-direction: column;
align-items: center;
.sports-view-matches {
margin-block-end: var(--space-small);
}
.sports-follow-teams-btn {
background: var(--newtab-background-color-secondary);
border-radius: var(--border-radius-xlarge);
}
}
.sports-follow-teams {
display: flex;
flex-direction: column;
gap: var(--space-small);
flex: 1;
}
.sports-country-search {
margin-block-end: var(--space-xsmall);
}
.sports-follow-teams-title {
font-size: var(--font-size-small);
align-self: center;
}
.sports-follow-teams-list {
display: flex;
flex-direction: column;
padding: var(--space-xsmall);
gap: var(--space-medium);
flex: 1;
overflow-y: auto;
}
.sports-follow-teams-row {
display: flex;
align-items: center;
gap: var(--space-small);
cursor: pointer;
&.is-disabled {
cursor: not-allowed;
opacity: 0.5;
moz-checkbox {
// Lets the row's not-allowed cursor show through; we can't reach the cursor on the input in the shadow DOM from outside.
pointer-events: none;
}
.sports-team-flag,
.sports-team-name {
cursor: not-allowed;
}
}
}
.sports-team-flag {
width: 30px;
height: 20px;
object-fit: cover;
flex-shrink: 0;
}
.sports-team-name {
flex: 1;
min-width: 0;
font-size: var(--font-size-root);
color: var(--text-color);
}
.sports-back-button {
grid-column: 1;
justify-self: start;
&:dir(rtl) {
transform: scaleX(-1);
}
}
.sports-matches-tabs {
display: flex;
}
.sports-matches-view {
height: 100%;
display: flex;
align-items: center;
}
.sports-matches-tab-panel {
width: 100%;
height: 100%;
}
.sports-matches-view > div > moz-button {
display: block;
margin-inline: auto;
}
.sports-matches-list-section > ul {
list-style: none;
padding-inline-start: 0;
margin-block: 0;
}
.sports-matches-list {
margin-block-start: var(--space-medium);
}
.sports-section-label {
color: var(--text-color-deemphasized);
font-size: var(--font-size-small);
}
.sports-matches-list-section {
display: flex;
flex-direction: column;
gap: var(--space-large);
}
@keyframes sports-section-label-live-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.sports-section-label-live {
color: var(--text-color-error);
font-weight: var(--font-weight-heading);
animation: sports-section-label-live-pulse 1.6s ease-in-out infinite;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
}
.sports-matches-list-section {
& + & {
border-block-start: 1px solid var(--border-color-deemphasized);
padding-block-start: calc(var(--space-large) + var(--space-xsmall));
margin-block-start: calc(var(--space-large) + var(--space-xsmall));
}
}
.sports-followed-only-toggle {
display: block;
margin-block-end: var(--space-small);
}
// Shared styles for the load-more rows shown in both the Upcoming and
// Results expanded lists. The .upcoming and .results variants below
// distinguish the two only for selection in tests; they share the same
// visual rules.
.sports-upcoming-loading-more,
.sports-results-loading-more {
text-align: center;
color: var(--text-color-deemphasized);
font-size: var(--font-size-small);
padding-block: var(--space-medium);
}
// 1px-tall element placed at the end of the upcoming / results matches
// list. The IntersectionObserver in SportsWidget.jsx watches this element
// and fires a fetch when it scrolls into view.
.sports-upcoming-load-more-sentinel,
.sports-results-load-more-sentinel {
height: 1px;
}
// This rule applies to the "Show less" button only when the full list of upcoming games or previous results is shown
.sports-matches-tab-panel:not(:has(.match-highlight-view)) > moz-button {
margin-block-start: var(--space-medium);
padding-block-end: var(--space-small);
}
.sports-matches-tab-panel:has(.match-highlight-view) {
height: 100%;
position: relative;
display: flex;
flex-direction: column;
}
.sports-matches-tab-panel:has(.match-highlight-view) > .sports-section-label,
.sports-matches-tab-panel:has(.match-highlight-view) > .sports-now-header {
position: absolute;
margin-block-start: var(--space-medium);
}
.sports-now-header {
inset-inline: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-small);
}
.sports-live-refresh-button {
margin-block-start: calc(-1 * var(--space-xsmall));
}
.medium-widget
.sports-matches-tab-panel:has(.match-highlight-view)
> .sports-live-refresh-button {
inset-inline: unset;
inset-inline-end: 0;
margin-block-start: 0;
}
.medium-widget
.sports-matches-tab-panel:has(.sports-live-pagination)
.sports-live-refresh-button {
inset-inline-end: var(--space-small);
inset-block-end: var(--space-xxsmall);
}
@media (prefers-reduced-motion: no-preference) {
.sports-live-refresh-button.is-spinning::part(button) {
animation: sports-live-refresh-spin 1s linear infinite;
}
}
@keyframes sports-live-refresh-spin {
to {
transform: rotate(360deg);
}
}
.large-widget
.sports-matches-tab-panel:has(.match-highlight-view)
> .match-highlight-view {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.sports-matches-tab-panel:has(.match-highlight-view) > moz-button {
position: absolute;
inset-block-end: 0;
inset-inline: 0;
width: fit-content;
}
.sports-upcoming-empty-info {
display: flex;
align-items: center;
gap: var(--space-small);
.sports-upcoming-empty-info-icon {
width: var(--icon-size);
height: var(--icon-size);
background: url('chrome://global/skin/icons/info.svg') no-repeat center / contain;
-moz-context-properties: fill;
fill: currentColor;
}
}
.sports-matches-tab {
flex: 1;
padding-block: var(--space-small);
border: none;
border-block-end: 2px solid transparent;
font-size: var(--font-size-small);
&:focus-visible {
outline: var(--focus-outline);
}
&.is-active {
font-weight: var(--font-weight-semibold);
border-block-end-color: var(--color-accent-primary);
}
&.is-disabled {
color: var(--text-color-disabled);
cursor: not-allowed;
pointer-events: none;
}
}
.sports-cancel-button {
grid-column: 3;
justify-self: end;
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--newtab-primary-action-background);
font-size: var(--font-size-small);
text-decoration: underline;
}
.sports-done-button {
margin-inline-start: auto;
position: sticky;
inset-inline-end: 0;
inset-block-end: var(--space-xxsmall);
}
// Key dates state
.sports-key-dates {
display: flex;
flex-direction: column;
.sports-key-dates-list {
list-style: none;
padding-inline-start: var(--space-xsmall);
margin-block-start: 0;
.sports-key-dates-item {
display: flex;
justify-content: space-between;
margin-block-end: var(--space-medium);
font-size: var(--font-size-small);
&:last-of-type {
margin-block-end: 0;
}
}
}
moz-button {
align-self: center;
}
}
// Holds the "View all" toggle hidden for one step, then restores it, so it
// reappears partway through the celebration instead of at the very end.
@keyframes sports-celebration-restore-view-all {
from {
visibility: hidden;
}
to {
visibility: visible;
}
}
@keyframes sports-celebration-border-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes sports-celebration-border-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}