/**
 * The Holiday Lookback interactive components, plus the report content
 * sections that sit under them.
 *
 * This file plus tokens.css and the component markup is the whole handoff.
 * There is no build step and no framework: drop these into any page.
 */

/* ---------------------------------------------------------------- card shell */

.card {
  box-sizing: border-box;
  margin: 0;
  font-family: var(--font-sans);
  color: var(--peppercorn);
  border-radius: var(--radius-card);
  position: relative;
}

.card *,
.card *::before,
.card *::after {
  box-sizing: border-box;
}

.card__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
}

.card__title {
  margin: 0;
  font-size: 21px;
  font-weight: 500;
  line-height: 1.35;
}

.card__subtitle {
  margin: 8px 0 0;
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
}

/* ---------------------------------------------------------------- toggle */

.toggle {
  display: inline-flex;
  gap: 6px;
  flex-shrink: 0;
  padding: 6px;
  background: var(--white);
  border: 1px solid var(--toggle-border);
  border-radius: var(--radius-pill);
}

/*
 * Visually hidden but still focusable and still a real form control, so the
 * toggle is keyboard and screen-reader operable and works without JavaScript.
 */
.toggle__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.toggle__pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 18px;
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--peppercorn);
  cursor: pointer;
  user-select: none;
  transition: background-color 120ms ease, color 120ms ease;
}

.toggle__input:checked + .toggle__pill {
  background: var(--peppercorn);
  color: var(--white);
}

.toggle__input:focus-visible + .toggle__pill {
  outline: 2px solid var(--pumpkin);
  outline-offset: 2px;
}

/*
 * The toggle drives which series is visible. Both are always in the markup;
 * only the presentation changes, so a crawler reads email and SMS either way.
 */
.card:has(.toggle__input[value="email"]:checked) [data-series="sms"],
.card:has(.toggle__input[value="sms"]:checked) [data-series="email"] {
  display: none;
}

/* ---------------------------------------------------------------- chart card */

.card--chart {
  background: var(--card-chart-bg);
  padding: 34px 40px 40px;
  max-width: 1280px;
}

.chart__scroll {
  /*
   * SVG text scales with the viewBox, so below roughly 900px the axis labels
   * stop being legible. Scrolling beats shrinking them into illegibility.
   */
  overflow-x: auto;
  margin-top: 32px;
}

.chart__plot {
  display: block;
  width: 100%;
  min-width: 900px;
  height: auto;
  overflow: visible;
}

/*
 * Set by enhance.js once it has measured a width it can read without scrolling.
 * Lets the fixed-viewBox chart shrink to fit the column instead of overflowing.
 * The trade-off is that the SVG text scales down with it; that ceiling is what
 * separates this progressive-enhancement path from the measured React build.
 */
.card--chart-fit .chart__plot {
  min-width: 0;
}

.chart__band-label {
  font-size: 16px;
  font-weight: 500;
  fill: var(--pumpkin);
}

.chart__gridline line {
  stroke: var(--rule);
  stroke-width: 1;
}

.chart__gridline text {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.28px;
  fill: var(--peppercorn);
}

.chart__xaxis text {
  font-size: 14px;
  font-weight: 400;
  fill: var(--peppercorn);
}

.chart__line {
  fill: none;
  stroke: var(--pumpkin);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.chart__dot {
  fill: var(--pumpkin);
}

/*
 * The value that sits above each dot. It can land on the line or on the BFCM
 * band, so it carries a stroke in the card colour, painted underneath the
 * glyphs, as a halo that keeps it readable without a filled box behind it.
 */
.chart__value {
  font-size: 13px;
  font-weight: 500;
  fill: var(--peppercorn);
  paint-order: stroke;
  stroke: var(--card-chart-bg);
  stroke-width: 3px;
  stroke-linejoin: round;
}

/* ---------------------------------------------------------------- stats card */

.card--stats {
  background: var(--card-stats-bg);
  padding: 24px 30px;
  max-width: 600px;
  min-height: 466px;
  display: flex;
  flex-direction: column;
}

.card__title--stats {
  max-width: 340px;
}

.stats {
  margin: 39px 0 0;
  display: flex;
  flex-direction: column;
  gap: 28px;
}

/*
 * The figure takes its own line under the label until the card is wide enough
 * to hold both, then moves up beside it and right-aligns. Both sides are nowrap,
 * and the row can still wrap on its own, so a figure that outgrows its line
 * drops rather than running off the card.
 */
.stats__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: 16px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--peppercorn);
}

.stats__label {
  font-size: 21px;
  font-weight: 500;
  line-height: 1.35;
  white-space: nowrap;
}

.stats__value {
  margin: 0;
  flex-basis: 100%;
  font-size: 26px;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  white-space: nowrap;
}

/* Narrower than any design covers, and the narrowest the card ever gets: gross
   revenue needs one size down to clear a 260px card. */
@media (max-width: 359px) {
  .stats__value {
    font-size: 21px;
  }
}

@media (min-width: 1024px) {
  .stats__value {
    flex-basis: auto;
    text-align: right;
  }
}

.card__footer {
  margin-top: auto;
  padding-top: 38px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.card__source {
  margin: 0;
  font-size: 13px;
  line-height: 1.3;
}

/* Source and disclaimer travel as a pair, so the logo can sit beside both lines
   on a wide card and drop below them on a narrow one. */
.card__credits {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.card__logo {
  display: block;
  width: 99px;
  height: 28px;
  flex-shrink: 0;
}

/* The stats card stacks its footer: source, footnote, then logo on its own line. */
.card--stats .card__footer {
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

.card--stats .card__source {
  line-height: 1.4;
}

.card--stats .card__logo {
  margin-top: 12px;
}

/* ---------------------------------------------------------------- tooltip */

/* Created by enhance.js. Everything above works without it. */
.chart__tooltip {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  padding: 8px 12px;
  border-radius: 8px;
  background: var(--peppercorn);
  color: var(--white);
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1.4;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, -100%);
  transition: opacity 120ms ease;
}

.chart__tooltip[data-visible="true"] {
  opacity: 1;
}

.chart__tooltip strong {
  font-weight: 500;
}

.chart__dot {
  transition: r 100ms ease;
}

.chart__dot[data-active="true"] {
  r: 8;
}

.chart__dot:focus {
  outline: none;
}

.chart__dot:focus-visible {
  outline: 2px solid var(--peppercorn);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- data table */

.data-table {
  margin-top: 32px;
}

.data-table table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  text-align: left;
}

.data-table caption {
  text-align: left;
  padding-bottom: 12px;
  font-size: 13px;
  line-height: 1.4;
  color: rgba(35, 30, 21, 0.7);
}

.data-table th,
.data-table td {
  padding: 10px 20px 10px 0;
  border-bottom: 1px solid var(--rule);
  font-weight: 400;
  vertical-align: baseline;
}

.data-table thead th {
  font-weight: 500;
  border-bottom-color: var(--peppercorn);
}

.data-table tbody th {
  font-weight: 500;
}

.data-table td {
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.data-table thead th[data-series] {
  text-align: right;
}

.data-table__note {
  margin: 12px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: rgba(35, 30, 21, 0.7);
  max-width: 78ch;
}

.card--chart .card__footer {
  margin-top: 28px;
  padding-top: 0;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

.card--chart .card__source {
  line-height: 1.4;
}

.card--chart .card__logo {
  margin-top: 12px;
}

/* Wide enough for the logo to sit beside two lines of credit text. */
@media (min-width: 1024px) {
  .card--chart .card__footer {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
    gap: 24px;
  }

  .card--chart .card__logo {
    margin-top: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .chart__dot,
  .chart__tooltip,
  .toggle__pill,
  .tactics__arrow,
  .tactics__ticker-icon {
    transition: none;
  }

  .tactics__photo--entering,
  .tactics__photo--entering img,
  .tactics__slide-text--entering {
    animation: none;
  }
}

/* ---------------------------------------------------------------- tactics */

/*
 * Ticker on top, then two columns at the design's 760:680 ratio, run full bleed
 * the way the report does at 1440. There is no max-width: the section takes
 * whatever width the page gives it, so the host page decides where its edges land.
 */
.tactics {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  width: 100%;
  font-family: var(--font-sans);
  color: var(--peppercorn);
}

.tactics *,
.tactics *::before,
.tactics *::after {
  box-sizing: border-box;
}

/* ---------------------------------------------------------------- ticker */

/*
 * A row: the marquee, then the play/pause control flush to the right edge. The
 * control's 45px is what sets the band's height, so it fills the band rather
 * than hanging over the section below it.
 */
.tactics__ticker {
  display: flex;
  align-items: center;
  background: var(--cavendish);
}

/*
 * Clipping the loop is this element's job alone, so the control's focus ring
 * lands outside anything that would crop it. Basis 0 rather than auto: the
 * track is max-content wide, which as an auto basis would price the control out
 * of the row.
 */
.tactics__ticker-viewport {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  padding-block: 10px;
}

.tactics__ticker-track {
  display: flex;
  width: max-content;
  animation: tactics-ticker 46s linear infinite;
}

.tactics__ticker-group {
  display: flex;
  flex-shrink: 0;
  align-items: center;
}

.tactics__ticker-item {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
}

.tactics__ticker-text {
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--peppercorn);
}

.tactics__ticker-star {
  display: block;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  margin-inline: 14px;
}

@keyframes tactics-ticker {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

/*
 * Playback is a paused animation rather than none, so stopping the loop leaves
 * it where it is instead of snapping it back to the start. Paused at rest looks
 * the same as no animation at all: the track holds the keyframe it opens on.
 */
@media (prefers-reduced-motion: reduce) {
  .tactics__ticker-track {
    animation-play-state: paused;
  }
}

/*
 * Written by ticker.js. Both outrank the reduced-motion default above whatever
 * the order, so a reader who asked for less motion can still start the loop.
 */
.tactics__ticker[data-ticker-state='playing'] .tactics__ticker-track {
  animation-play-state: running;
}

.tactics__ticker[data-ticker-state='paused'] .tactics__ticker-track {
  animation-play-state: paused;
}

/* Revealed only once ticker.js is running. See the note in that file. */
.tactics__ticker-toggle {
  display: none;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 45px;
  height: 45px;
  padding: 0;
  border: 0;
  background: var(--peppercorn);
  cursor: pointer;
}

.tactics__ticker--live .tactics__ticker-toggle {
  display: flex;
}

/*
 * Inset ring: the control sits on the section's right edge, and the report has
 * no say in what the host page puts against it, so an outset ring risks landing
 * where it cannot be seen. Cavendish because the box behind it is peppercorn.
 */
.tactics__ticker-toggle:focus-visible {
  outline: 2px solid var(--cavendish);
  outline-offset: -5px;
}

.tactics__ticker-icon {
  display: block;
  transition: opacity 150ms ease;
}

.tactics__ticker-toggle:hover .tactics__ticker-icon {
  opacity: 0.75;
}

/* The glyph is the action, not the state, so it is the opposite of what runs. */
.tactics__ticker-toggle[data-state='playing'] .tactics__ticker-icon--play,
.tactics__ticker-toggle[data-state='paused'] .tactics__ticker-icon--pause {
  display: none;
}

/*
 * Percentage bases rather than grow factors: the figure's aspect-ratio below
 * gives the media column an intrinsic width, which grow factors would fold
 * into the split and skew it off 760:680.
 */
.tactics__row {
  display: flex;
  align-items: stretch;
  width: 100%;
}

.tactics__panel {
  flex: 0 1 52.7778%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--light-pumpkin);
  padding: clamp(40px, 4.9vw, 70px) clamp(28px, 5.6vw, 80px) clamp(48px, 5.6vw, 80px);
}

.tactics__content {
  display: flex;
  flex-direction: column;
  gap: 24px;
  max-width: 600px;
}

.tactics__heading {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(28px, 2.5vw, 36px);
  font-weight: 300;
  line-height: 1.1;
}

.tactics__intro {
  font-size: 16px;
  line-height: 1.5;
}

.tactics__intro p {
  margin: 0;
}

/* The design's blank line between the intro and the lead-in is one 16/1.5 line. */
.tactics__intro p + p {
  margin-top: 24px;
}

.tactics__lead-in {
  font-weight: 600;
}

.tactics__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.tactics__item {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding-bottom: 8px;
}

/* Optically centred on the first line of the text next to it. */
.tactics__check {
  display: block;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  margin-top: 6px;
}

.tactics__item-text {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
}

.tactics__item-text a {
  color: inherit;
  text-decoration: underline;
  text-underline-position: from-font;
}

.tactics__media {
  flex: 0 1 47.2222%;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/*
 * The aspect ratio sets the figure's natural height; it then grows so the
 * column matches the taller panel and the callout stays bottom-aligned.
 */
/*
 * The explicit width keeps aspect-ratio resolving width -> height, not the
 * reverse. Peppercorn rather than transparent because the entering photo starts
 * scaled down, and this is the stage it grows out of.
 */
.tactics__figure {
  position: relative;
  flex: 1 1 auto;
  width: 100%;
  aspect-ratio: 680 / 436;
  min-height: 220px;
  overflow: hidden;
  background: var(--peppercorn);
}

/*
 * Hidden by default and revealed only when active, so a missing or empty
 * data-active can never fail open and show the whole stack at once.
 */
.tactics__photo {
  position: absolute;
  inset: 0;
  display: none;
}

/*
 * The outgoing photo does not move. It stays put at full size and full opacity
 * while the incoming one slides over the top of it, which is why no gap can open
 * between them, then it is dropped once it is fully covered.
 */
.tactics__photo--leaving {
  display: block;
}

/* Above the leaving photo, so the incoming card slides over it, not under. */
.tactics__photo[data-active="true"] {
  display: block;
  z-index: 1;
}

.tactics__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Reproduces each photo's crop in Figma; set per slide in the markup. */
  object-position: var(--tactics-crop, 50% 50%);
}

.tactics__callout {
  background: var(--peppercorn);
  color: var(--white);
  padding: clamp(28px, 2.8vw, 40px) clamp(28px, 3.5vw, 50px) clamp(32px, 3.5vw, 50px);
}

.tactics__callout-inner {
  display: flex;
  align-items: flex-end;
  gap: 10px;
}

/*
 * All three slides are stacked in a single grid cell, so the callout is always
 * as tall as the longest stat and the section keeps one height for the whole
 * carousel. Swapping them in and out of flow instead would let the two-line
 * stat push the panel taller than the one-line ones.
 */
.tactics__stats {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
}

/*
 * Hidden with visibility rather than display, which is what keeps each slide's
 * height in the cell. It still takes them out of the accessibility tree, so the
 * live region announces only the stat on screen.
 */
.tactics__slide-text {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  gap: 16px;
  visibility: hidden;
}

.tactics__slide-text[data-active="true"] {
  visibility: visible;
}

.tactics__eyebrow {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.13;
  letter-spacing: 0.32px;
  text-transform: uppercase;
}

.tactics__stat {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(26px, 2.2vw, 32px);
  font-weight: 400;
  line-height: 1.2;
}

/* Revealed only once carousel.js is running. See the note in that file. */
.tactics__arrows {
  display: none;
  gap: 12px;
  flex-shrink: 0;
}

.tactics__media--live .tactics__arrows {
  display: flex;
}

.tactics__arrow {
  display: block;
  width: 43px;
  height: 43px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: 50%;
  transition: transform 150ms ease, opacity 150ms ease;
}

.tactics__arrow:hover {
  opacity: 0.8;
  transform: scale(1.06);
}

.tactics__arrow:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 3px;
}

.tactics__arrow img {
  display: block;
  width: 100%;
  height: 100%;
}

.tactics__arrow--prev {
  transform: rotate(180deg);
}

.tactics__arrow--prev:hover {
  transform: rotate(180deg) scale(1.06);
}

/* ---------------------------------------------------------------- carousel motion */

/*
 * These four are the whole tuning surface. The photo travels in over
 * --tactics-slide-ms, sits still for --tactics-hold-ms, then pulls back from
 * --tactics-zoom-from to fit over --tactics-zoom-ms.
 *
 * Each is a real duration rather than a keyframe percentage because the travel
 * and the zoom run on separate elements: the slide on .tactics__photo, the zoom
 * on the img inside it. Two animations on one element cannot both drive
 * transform — the last one declared wins outright — so splitting them across the
 * pair is what lets either be retimed without touching the other.
 */
.tactics__media {
  --tactics-slide-ms: 1000ms;
  --tactics-hold-ms: 100ms;
  --tactics-zoom-ms: 500ms;
  --tactics-zoom-from: 1.00;
}

/*
 * The photo only travels. It enters from off the right and decelerates into
 * place; the easing has no overshoot, so it never passes its landing point.
 * Going back mirrors the travel via --tactics-from, set by carousel.js per click.
 */
@keyframes tactics-slide-in {
  from {
    transform: translateX(var(--tactics-from, 100%));
  }

  to {
    transform: translateX(0);
  }
}

/*
 * The img only zooms. Its backwards fill holds the zoomed-in scale through the
 * slide and the hold, which is what keeps the frame covered before the zoom
 * begins: the photo is never smaller than its frame, never rounds its corners
 * and never fades, and each of those is a way to expose the stage behind it.
 */
@keyframes tactics-zoom-fit {
  from {
    transform: scale(var(--tactics-zoom-from, 1.14));
  }

  to {
    transform: scale(1);
  }
}

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

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.tactics__photo--entering {
  animation: tactics-slide-in var(--tactics-slide-ms) cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* The delay is the slide plus the hold, so the zoom starts once travel has settled. */
.tactics__photo--entering img {
  animation: tactics-zoom-fit var(--tactics-zoom-ms)
    calc(var(--tactics-slide-ms) + var(--tactics-hold-ms)) cubic-bezier(0.33, 1, 0.68, 1) both;
}

.tactics__slide-text--entering {
  animation: tactics-text-in 380ms 90ms ease-out both;
}

@media (max-width: 900px) {
  .tactics__row {
    flex-direction: column;
  }

  /* Stacked, the figure no longer absorbs slack, so the ratio alone sizes it. */
  .tactics__figure {
    flex: 0 0 auto;
    aspect-ratio: 3 / 2;
    min-height: 0;
  }
}

/* Narrower than this the arrows starve the stat of width, so they drop below it. */
@media (max-width: 560px) {
  .tactics__callout-inner {
    flex-direction: column;
    align-items: stretch;
    gap: 24px;
  }

  .tactics__arrows {
    align-self: flex-end;
  }
}
