/**
 * styles.css
 * ---------------------------------------------------------------------------
 * Structure:
 *   1. Design tokens (spacing, type, radius, motion) — colours live in
 *      /themes/light.css so a future dark.css can override just those.
 *   2. Reset / base
 *   3. Layout primitives
 *   4. Components (cards, KPI tiles, tables, buttons, header)
 *   5. Section-specific tweaks
 *   6. Responsive breakpoints
 * ---------------------------------------------------------------------------
 */

/* ============================== 1. TOKENS ============================== */

:root {
  /* Spacing scale (4px base) */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;

  /* Radius */
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 20px;
  --radius-full: 999px;

  /* Typography */
  --font-sans: "Google Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace;

  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-md: 16px;
  --text-lg: 20px;
  --text-xl: 28px;
  --text-2xl: 36px;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  --leading-tight: 1.2;
  --leading-normal: 1.5;

  /* Motion */
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --duration-fast: 120ms;
  --duration-base: 200ms;
  --duration-slow: 360ms;

  /* Layout */
  --content-max-width: 1280px;
  --sidebar-width: 240px;
}

/* ============================== 2. RESET / BASE ============================== */

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

html, body {
  margin: 0;
  padding: 0;
}

/* Stops iOS Safari "text inflation": it enlarges text in a block it thinks is
   too wide for the screen -- which long no-wrap table cells (e.g. company
   names in Audience Demographics) trigger -- so some names rendered at about
   double the size of the rest. 100% = never auto-resize. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  background: var(--color-bg);
  color: var(--color-text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background var(--duration-slow) var(--ease-standard),
              color var(--duration-slow) var(--ease-standard);
}

h1, h2, h3, h4, p, ul, ol {
  margin: 0;
}

img { max-width: 100%; display: block; }
/* The rule above overrides the browser's native [hidden]{display:none} for
   every <img> (author styles always beat the user-agent stylesheet,
   regardless of selector specificity) -- without this, an <img hidden>
   stays visually displayed. Only #channel-logo currently relies on the
   hidden attribute to toggle visibility; this restores that behaviour. */
img[hidden] { display: none; }

button {
  font-family: inherit;
  cursor: pointer;
}

a { color: inherit; text-decoration: none; }

/* ============================== 3. LAYOUT PRIMITIVES ============================== */

.page {
  position: relative;
  z-index: 1;
  max-width: var(--content-max-width);
  margin: 0 auto;
  padding: var(--space-8) var(--space-6) var(--space-16);
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}

.grid {
  display: grid;
  gap: var(--space-5);
}

.grid--kpis {
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

.grid--creatives {
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  /* Thumbnails now size to each creative's real aspect ratio (see
     .creative-card__thumb-img below), so cards in the same row are no
     longer equal height -- without this, grid's default stretch would
     force every card to match the tallest one, leaving ugly gaps under
     shorter/wider images. */
  align-items: start;
}

.grid--2col {
  grid-template-columns: 2fr 1fr;
}

.section {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.section__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.section__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
}

.section__subtitle {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  /* Date ranges stay on one line; a long section title wraps instead. */
  white-space: nowrap;
  flex-shrink: 0;
}

/* Creative Performance is the one section with two subtitle pieces at once
   (its channel label, "Meta Ads", plus the date range) -- stacked the same
   way .header__meta stacks the top header's two lines, rather than trying
   to fit both on one line next to a section title. */
.section__subtitle-group {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

/* ============================== 4. COMPONENTS ============================== */

/* --- Card, the base surface for nearly everything --- */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6);
  transition: box-shadow var(--duration-base) var(--ease-standard),
              transform var(--duration-base) var(--ease-standard),
              background var(--duration-slow) var(--ease-standard),
              border-color var(--duration-slow) var(--ease-standard);
}

.card--hoverable:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

/* --- Top header bar --- */
.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  padding-bottom: var(--space-2);
}

.header__brands {
  display: flex;
  align-items: center;
  gap: var(--space-5);
}

.header__divider {
  width: 1px;
  height: 24px;
  background: var(--color-border-strong);
}

/* Fixed height, auto width -- preserves each logo's real aspect ratio
   instead of stretching it into a hardcoded box. .skeleton reserves a
   sensible width while loading; it's removed once the image src is set. */
.header__agency-logo {
  height: 32px;
  width: auto;
  max-width: 160px;
  object-fit: contain;
}
.header__agency-logo.skeleton { width: 120px; }

.header__client-logo {
  height: 32px;
  width: 32px;
  border-radius: var(--radius-sm);
  object-fit: cover;
}

.header__client {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.header__client-name {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

/* Channel platform mark (Google/Meta/Microsoft/TikTok/LinkedIn Ads) --
   shown only while a single channel is selected in the channel filter.
   Fixed square box rather than auto-width: the five source SVGs have
   different native aspect ratios (Meta's mark is ~1.5:1, TikTok's ~0.88:1,
   the rest square), so sizing by height alone made each logo render at a
   different width -- shifting the header's overall width depending on
   which channel was selected. object-fit: contain letterboxes each mark
   within the same box instead of cropping it. */
.header__channel-logo {
  width: 28px;
  height: 28px;
  object-fit: contain;
}

.header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.header__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
}

.header__daterange {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--color-text-primary);
}

.header__updated {
  font-size: var(--text-xs);
  color: var(--color-text-tertiary);
}

/* --- Theme toggle --- */
.theme-toggle {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--duration-base) var(--ease-standard),
              border-color var(--duration-base) var(--ease-standard),
              color var(--duration-base) var(--ease-standard),
              transform var(--duration-fast) var(--ease-standard);
}

.theme-toggle:hover { border-color: var(--color-border-strong); color: var(--color-accent-text); }
.theme-toggle:active { transform: scale(0.92); }
.theme-toggle__icon { width: 17px; height: 17px; }
.theme-toggle__icon--moon { display: none; }
html[data-theme="dark"] .theme-toggle__icon--sun { display: none; }
html[data-theme="dark"] .theme-toggle__icon--moon { display: block; }

.badge {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  white-space: nowrap;
}

.badge--positive { background: var(--color-positive-soft); color: var(--color-positive); }
.badge--negative { background: var(--color-negative-soft); color: var(--color-negative); }
.badge--warning  { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--neutral  { background: var(--color-accent-soft); color: var(--color-accent-text); }

/* --- Date range filter --- */
.date-filter {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.date-filter__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.date-filter__row--compare,
.date-filter__row--channel,
.date-filter__row--search {
  justify-content: flex-start;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.date-filter__compare-label {
  font-size: var(--text-sm);
  color: var(--color-text-tertiary);
  white-space: nowrap;
}

.date-filter__compare-summary {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}

.date-filter__compare-mode {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
}

.date-filter__presets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.date-filter__preset {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  transition: background var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}

.date-filter__preset:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text-primary);
}

.date-filter__preset.is-active {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
}

.date-filter__custom {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.date-filter__custom[hidden] {
  display: none;
}

.date-filter__input {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: inherit;
  font-size: var(--text-sm);
}

.date-filter__sep {
  color: var(--color-text-tertiary);
}

.date-filter__apply {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: none;
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  transition: opacity var(--duration-fast) var(--ease-standard);
}

.date-filter__apply:hover {
  opacity: 0.85;
}

@media (max-width: 640px) {
  .date-filter__custom { flex-wrap: wrap; }
}

/* Campaign name search -- reuses .date-filter__input's own look (same
   border/radius/font as the date pickers) so it doesn't read as a
   different control language, just widened to fit a campaign name and
   given room for the clear (×) button. */
.date-filter__search-wrap {
  position: relative;
  flex: 1 1 240px;
  min-width: 180px;
  max-width: 360px;
}

.date-filter__search-input {
  width: 100%;
  padding: var(--space-2) var(--space-8) var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: inherit;
  font-size: var(--text-sm);
}

.date-filter__search-input:focus {
  outline: none;
  border-color: var(--color-accent);
}

.date-filter__search-clear {
  position: absolute;
  right: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  border-radius: var(--radius-full);
  border: none;
  background: transparent;
  color: var(--color-text-tertiary);
  font-size: var(--text-base);
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color var(--duration-fast) var(--ease-standard);
}

.date-filter__search-clear:hover {
  color: var(--color-text-primary);
}

.date-filter__search-clear[hidden] {
  display: none;
}

/* --- KPI tiles --- */
.kpi-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* Columns must never resize to fit a tile's content (a long, changing
     number used to nudge the two mobile columns wider/narrower while it
     counted). min-width: 0 stops the tile demanding width; as a size
     container, its inner width (100cqi) is what the figure below fits to. */
  min-width: 0;
  container-type: inline-size;
}

.kpi-card__label {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  font-weight: var(--weight-medium);
}

.kpi-card__value {
  font-size: var(--text-xl);
  /* Shrinks a long figure (e.g. 689,978 or 1,234,567) to fit the tile's
     width -- roughly 0.6em per character -- never above the normal size and
     never below 18px. --kpi-chars is set per tile in kpis.js. */
  font-size: clamp(18px, calc(100cqi / (var(--kpi-chars, 7) * 0.6)), var(--text-xl));
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
  white-space: nowrap;
}

.kpi-card__footer {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-top: var(--space-1);
}

/* Sparkline under each KPI tile -- stroke colour follows the trend badge. */
.kpi-card__spark {
  display: block;
  width: 100%;
  height: 22px;
  margin-top: var(--space-1);
  overflow: visible;
}
.kpi-card__spark path {
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.kpi-card__spark--positive path { stroke: var(--color-positive); }
.kpi-card__spark--negative path { stroke: var(--color-negative); }
.kpi-card__spark--warning path { stroke: var(--color-warning); }
.kpi-card__spark--neutral path { stroke: var(--color-text-tertiary); }

.kpi-card__target {
  font-size: var(--text-xs);
  color: var(--color-text-tertiary);
}

/* --- Chart cards --- */
.chart-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.chart-card__canvas-wrap {
  position: relative;
  height: 220px;
}

.chart-card__canvas-wrap--tall {
  height: 340px;
}

/* Shown in place of the chart when there's nothing to plot. */
.chart-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--space-4);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}

.chart-toggle-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chart-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  font-family: inherit;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
}

.chart-toggle::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--toggle-color, var(--color-text-tertiary));
  opacity: 0.4;
  transition: opacity var(--duration-fast) var(--ease-standard);
}

.chart-toggle:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text-primary);
}

.chart-toggle.is-active {
  border-color: var(--toggle-color, var(--color-accent));
  color: var(--color-text-primary);
}

.chart-toggle.is-active::before {
  opacity: 1;
}

/* --- Transaction Drilldown heatmap --- */
/* Day of Week x Hour grid, shaded by intensity -- see
   assets/modules/transactionDrilldown.js for how --heatmap-intensity (a
   0%-100% custom property set per cell) and the "strong" text-colour
   threshold are computed. color-mix() blends the client's own accent
   colour into the page background (--color-bg, not --color-surface --
   see .heatmap-cell below for why), so an empty cell is a faint neutral
   square rather than invisible, and a maximum cell is solid accent -- no
   separate heatmap colour palette to keep in sync with each client's
   branding, and it follows light/dark theme automatically.

   The grid fills the full card width (colgroup in transactionDrilldown.js
   fixes the hour-label column's width, table-layout: fixed shares the rest
   equally across the 7 day columns) -- a heatmap that only occupies a
   fraction of a wide card reads as unfinished, not intentional. Cells use
   border-spacing (not table borders) for the gap between them, giving the
   rounded, discrete "calendar heatmap" look rather than a bordered grid. */
.heatmap-wrap {
  overflow-x: auto;
  margin-top: var(--space-5);
}

table.heatmap-table {
  width: 100%;
  min-width: 640px; /* below this the grid gets cramped -- scrolls instead, same as .table-wrap */
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: var(--space-1);
  font-size: var(--text-sm);
}

.heatmap-col--label {
  width: 60px;
}

.heatmap-table th, .heatmap-table td {
  height: 38px;
  text-align: center;
  padding: 0;
}

.heatmap-day-label {
  color: var(--color-text-secondary);
  font-weight: var(--weight-semibold);
  font-size: var(--text-sm);
  padding-bottom: var(--space-2) !important;
}

.heatmap-hour-label {
  color: var(--color-text-tertiary);
  font-weight: var(--weight-medium);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  text-align: right;
  padding-right: var(--space-3) !important;
  white-space: nowrap;
}

.heatmap-cell {
  background-color: color-mix(in srgb, var(--color-accent) var(--heatmap-intensity, 0%), var(--color-bg));
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
  cursor: default;
  transition: background-color var(--duration-base) var(--ease-standard),
              transform var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard);
}

/* A little lift on hover doubles as feedback that the exact value is
   available (via the title tooltip) even though most cells show no text.
   Gated to real hover-capable pointers (a mouse, not a touchscreen) --
   without this, a tap on mobile Chrome (which simulates :hover on touch,
   unlike desktop-only interaction) combined with this rule's `transform`
   sitting on top of the cell's color-mix()-computed background has been
   seen to leave that cell stuck rendering flat white/near-white after the
   tap, as if its --heatmap-intensity had been reset to 0 -- a Chromium
   repaint quirk with that specific combination, not something Safari
   reproduces. A `title` tooltip isn't a reliable way to reveal a value on
   touch anyway, so there's nothing this hover effect was actually doing for
   a touch user -- restricting it to devices that can genuinely hover
   removes the trigger entirely rather than trying to patch around it. */
@media (hover: hover) {
  .heatmap-cell:hover {
    transform: scale(1.12);
    box-shadow: var(--shadow-sm);
    position: relative;
    z-index: 1;
  }
}

/* Once the accent tint is dark/saturated enough, the app's own "text on
   accent" colour (already computed per client/theme by theme.js) reads
   better than the normal body text colour -- same contrast reasoning
   applied to accent-coloured buttons/badges elsewhere. */
.heatmap-cell--strong {
  color: var(--color-text-on-accent);
}

.heatmap-legend {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.heatmap-legend__label {
  font-size: var(--text-xs);
  color: var(--color-text-tertiary);
}

.heatmap-legend__scale {
  display: flex;
  gap: var(--space-1);
}

.heatmap-legend__swatch {
  width: 16px;
  height: 16px;
  border-radius: var(--radius-sm);
  background-color: color-mix(in srgb, var(--color-accent) var(--heatmap-intensity, 0%), var(--color-bg));
}

/* --- Tables --- */
.table-wrap {
  overflow-x: auto;
  container-type: inline-size; /* lets the empty-state message size itself to the visible width, see .data-table__empty-msg */
}

table.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.data-table th, .data-table td {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  white-space: nowrap;
}

.data-table th {
  color: var(--color-text-tertiary);
  font-weight: var(--weight-medium);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Campaign Performance only -- click a header to rank by that metric (see
   tables.js's renderCampaignTable). Monthly Comparison's headers are plain
   <th> with none of these classes, so they stay non-interactive. */
.data-table__th--sortable {
  cursor: pointer;
  user-select: none;
  transition: color var(--duration-fast) var(--ease-standard);
}

.data-table__th--sortable:hover {
  color: var(--color-text-secondary);
}

.data-table__th--sortable.is-sorted {
  color: var(--color-accent-text);
}

.data-table tbody tr {
  transition: background var(--duration-fast) var(--ease-standard);
}

.data-table tbody tr:hover {
  background: var(--color-bg);
}

.data-table tbody tr:last-child td {
  border-bottom: none;
}

.table-cell__value {
  margin-right: var(--space-2);
}

.table-cell__change {
  font-size: 11px;
  padding: 2px var(--space-2);
}

/* Shown next to a Campaign Performance row's name when an active ad set
   search has narrowed that row down to just one or more specific ad sets --
   see tables.js's renderCampaignTable() / app.js's withAdSetNarrowing().
   Same accent-soft treatment as .badge--neutral (it's flagging the same
   kind of thing -- "this reflects an active filter", not a positive/negative
   change), but its own class since it sits inline after a table-cell value
   rather than as a standalone badge. */
.table-cell__tag {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 1px var(--space-2);
  border-radius: var(--radius-full);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  vertical-align: middle;
}

/* --- Creative performance cards --- */
.creative-card {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
  overflow: hidden;
}

.creative-card__thumb {
  width: 100%;
  background: var(--color-bg);
  overflow: hidden;
}

.creative-card__thumb-img {
  /* Sized to the creative's own natural aspect ratio -- no fixed box, no
     object-fit crop. A portrait Story ad stays portrait, a square post
     stays square, a landscape one stays landscape. */
  display: block;
  width: 100%;
  height: auto;
}

.creative-card__thumb-fallback {
  /* Unlike the real image above, there's no natural size to fall back to
     here, so this keeps a fixed box just for the "no preview" placeholder. */
  width: 100%;
  aspect-ratio: 16 / 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  color: var(--color-text-tertiary);
  font-size: var(--text-xs);
}

.creative-card__thumb-fallback svg { width: 28px; height: 28px; }

.creative-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
}

.creative-card__heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
}

.creative-card__name {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  min-width: 0; /* lets long names wrap instead of squeezing the badge next to them */
  /* Ad names are often underscore_separated with no spaces to wrap at --
     without this, a long one overflows straight through the badge next to it
     instead of breaking onto a second line. */
  overflow-wrap: anywhere;
}

.creative-card__stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}

/* Whether the stats grid ends on an odd count depends on hasRevenue (ROAS
   adds one more) -- rather than ever leaving the last row's second column
   empty next to a lone stat, that stat spans the full row instead. */
.creative-card__stats > .creative-card__stat:last-child:nth-child(odd) {
  grid-column: 1 / -1;
}

.creative-card__stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.creative-card__stat-label {
  font-size: var(--text-xs);
  color: var(--color-text-tertiary);
}

.creative-card__stat-value {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}

/* Unlike the table cells, the value here isn't wrapped in .table-cell__value
   (which carries the margin-right spacing) -- it's plain text right up
   against the badge. Scoped here rather than editing .table-cell__change
   directly so the Campaign Performance table's badges are untouched. */
.creative-card__stat-value .table-cell__change {
  margin-left: var(--space-2);
  font-size: 10px;
  padding: 1px var(--space-2);
}

.creative-card__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--color-accent-text);
  transition: opacity var(--duration-fast) var(--ease-standard);
}

.creative-card__link:hover { opacity: 0.75; }
.creative-card__link svg { width: 14px; height: 14px; }

/* --- Lists (insights / next steps) --- */
.insight-list, .steps-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.insight-item, .step-item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.insight-item__dot {
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  margin-top: 6px;
  border-radius: var(--radius-full);
  background: var(--color-accent);
}

.step-item__index {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* --- Commentary --- */
.commentary-text {
  font-size: var(--text-md);
  line-height: var(--leading-normal);
  color: var(--color-text-secondary);
}

/* --- Loading / empty states --- */
.skeleton {
  background: linear-gradient(90deg, var(--color-border) 25%, var(--color-bg) 37%, var(--color-border) 63%);
  background-size: 400% 100%;
  animation: skeleton-shimmer 1.4s ease infinite;
  border-radius: var(--radius-sm);
}

@keyframes skeleton-shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

.fade-in {
  animation: fade-in var(--duration-slow) var(--ease-standard) both;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ============================== 5b. POLISH (empty states, skeletons, table extras, motion) ============================== */

/* Note under the search box when a search matches nothing at all. */
.date-filter__search-note {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  align-self: center;
}
.date-filter__search-note[hidden] { display: none; }

/* Empty table body row (e.g. a search that matched nothing). */
.data-table__empty td {
  padding: 0;
  white-space: normal;
  border-bottom: none;
}
/* The empty-state cell spans every column, and a table can be wider than its
   card (it scrolls sideways), so centring on the cell would centre on the
   whole table, off to the right of what's visible. Instead the message is
   sticky to the left edge and exactly as wide as the visible card
   (.table-wrap is a size container, so 100cqw = its visible width). */
.data-table__empty-msg {
  position: sticky;
  left: 0;
  box-sizing: border-box;
  width: 100cqw;
  padding: var(--space-8) var(--space-4);
  text-align: center;
  color: var(--color-text-secondary);
}
.data-table tbody tr.data-table__empty:hover { background: transparent; }

/* --- Loading skeletons (replaced by real content on first render) --- */
.sk-line { display: block; border-radius: var(--radius-sm); }
.sk-line--label { height: 12px; width: 55%; }
.sk-line--value { height: 28px; width: 75%; }
.sk-line--badge { height: 20px; width: 40%; border-radius: var(--radius-full); }
.sk-line--row { height: 16px; width: 100%; }
/* Generic skeleton for EVERY section that doesn't ship its own (the four
   above-the-fold ones are marked data-own-skeleton in index.html). While
   #page-root has data-loading, such a section shows its heading plus one
   shimmer block in place of its content -- so any optional or future section
   (Creative Performance, Transaction Drilldown, Audience Demographics, ...)
   gets a loading state automatically, with no per-section markup. */
#page-root[data-loading] .section:not([data-own-skeleton]) > :not(.section__header) { display: none; }
#page-root[data-loading] .section:not([data-own-skeleton])::after {
  content: "";
  display: block;
  height: 220px;
  border-radius: var(--radius-md);
  background: linear-gradient(90deg, var(--color-border) 25%, var(--color-bg) 37%, var(--color-border) 63%);
  background-size: 400% 100%;
  animation: skeleton-shimmer 1.4s ease infinite;
}
.kpi-card--skeleton { pointer-events: none; }
.chart-skeleton {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
}
.data-table__skeleton td { padding: var(--space-4); border-bottom: none; }
.data-table tbody tr.data-table__skeleton:hover { background: transparent; }

/* --- Campaign Performance: scroll inside the card so the header row can
   stick. Only kicks in once there are enough campaigns to need it. --- */
.table-wrap--scroll {
  max-height: 560px;
  overflow-y: auto;
}
.table-wrap--scroll .data-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--color-surface);
  /* border-collapse drops borders on sticky cells while scrolling, so the
     header's bottom rule is drawn as an inset shadow instead. */
  box-shadow: inset 0 -1px 0 var(--color-border);
  border-bottom: none;
}

/* Greyed-out text for a cell that has no real figure (e.g. "Below reporting minimum"). */
.data-table td.table-cell--muted { color: var(--color-text-tertiary); }

/* Audience Demographics bar chart: top values by share of impressions. */
.demographics-chart {
  margin-top: var(--space-5);
}
.demographics-chart[hidden] { display: none; }
.demographics-chart__title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--color-text-secondary);
  margin-bottom: var(--space-3);
}
.demographics-bars {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.demographics-bar {
  display: grid;
  grid-template-columns: minmax(110px, 240px) 1fr 56px;
  align-items: center;
  gap: var(--space-4);
  font-size: var(--text-sm);
}
.demographics-bar__label {
  color: var(--color-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.demographics-bar__track {
  height: 10px;
  border-radius: var(--radius-full);
  background: var(--color-accent-soft);
  overflow: hidden;
}
.demographics-bar__track > i {
  display: block;
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--color-accent);
  transform-origin: left center;
  animation: demographics-bar-grow 550ms var(--ease-standard) both;
}
.demographics-bar__value {
  text-align: right;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.demographics-chart__note {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  color: var(--color-text-tertiary);
}
@keyframes demographics-bar-grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
@media (max-width: 640px) {
  /* Label on its own line, bar + % underneath, so long names stay readable. */
  .demographics-bar {
    grid-template-columns: 1fr 52px;
    grid-template-areas: "label label" "track value";
    row-gap: var(--space-1);
  }
  .demographics-bar__label { grid-area: label; }
  .demographics-bar__track { grid-area: track; }
  .demographics-bar__value { grid-area: value; }
}

/* Audience Demographics: search + result count above the (scrollable) list. */
.demographics-tools {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  margin-top: var(--space-4);
  margin-bottom: var(--space-2);
}
.demographics-count {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}

/* Spend-share bar under the Spend figure in Campaign Performance. */
.table-cell__bar {
  display: block;
  height: 3px;
  margin-top: 6px;
  border-radius: 2px;
  background: var(--color-accent-soft);
  overflow: hidden;
}
.table-cell__bar > i {
  display: block;
  height: 100%;
  border-radius: 2px;
  background: var(--color-accent);
}

/* Numbers line up and don't jitter while counting up. */
.kpi-card__value,
.data-table td,
.creative-card__stat-value,
.table-cell__change {
  font-variant-numeric: tabular-nums;
}

/* Staggered entrance for KPI tiles (the tiles already use .fade-in). */
.grid--kpis > .kpi-card:nth-child(1) { animation-delay: 0ms; }
.grid--kpis > .kpi-card:nth-child(2) { animation-delay: 45ms; }
.grid--kpis > .kpi-card:nth-child(3) { animation-delay: 90ms; }
.grid--kpis > .kpi-card:nth-child(4) { animation-delay: 135ms; }
.grid--kpis > .kpi-card:nth-child(5) { animation-delay: 180ms; }
.grid--kpis > .kpi-card:nth-child(6) { animation-delay: 225ms; }
.grid--kpis > .kpi-card:nth-child(7) { animation-delay: 270ms; }
.grid--kpis > .kpi-card:nth-child(8) { animation-delay: 315ms; }
.grid--kpis > .kpi-card:nth-child(9) { animation-delay: 360ms; }
.grid--kpis > .kpi-card:nth-child(10) { animation-delay: 405ms; }
.grid--kpis > .kpi-card:nth-child(11) { animation-delay: 450ms; }
.grid--kpis > .kpi-card:nth-child(12) { animation-delay: 495ms; }
.grid--kpis > .kpi-card:nth-child(13) { animation-delay: 540ms; }
.grid--kpis > .kpi-card:nth-child(14) { animation-delay: 585ms; }
.grid--kpis > .kpi-card:nth-child(15) { animation-delay: 630ms; }
.grid--kpis > .kpi-card:nth-child(16) { animation-delay: 675ms; }
.grid--kpis > .kpi-card:nth-child(17) { animation-delay: 720ms; }
.grid--kpis > .kpi-card:nth-child(18) { animation-delay: 765ms; }
.grid--kpis > .kpi-card:nth-child(19) { animation-delay: 810ms; }
.grid--kpis > .kpi-card:nth-child(20) { animation-delay: 855ms; }

@media (prefers-reduced-motion: reduce) {
  .fade-in,
  .demographics-bar__track > i,
  .skeleton,
  #page-root[data-loading] .section:not([data-own-skeleton])::after,
  .grid--kpis > .kpi-card { animation: none !important; }
  .card--hoverable:hover { transform: none; }
}

/* --- Brand wash: a faint tint of the client's accent colour that fades
   from the top of the page into the normal background. Full width (it's on
   the body, not the centred .page), behind all content. --- */
body.has-brand-wash::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 380px;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(180deg, color-mix(in srgb, var(--color-accent) 9%, transparent) 0%, transparent 100%);
}
[data-theme="dark"] body.has-brand-wash::before {
  background: linear-gradient(180deg, color-mix(in srgb, var(--color-accent) 14%, transparent) 0%, transparent 100%);
}

/* --- Chart options row (under the metric pills): a small switch for
   "Compare to previous period", kept visually apart from the metric pills. --- */
.chart-options { display: flex; }
.chart-options[hidden] { display: none; }
.chart-compare {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0;
  border: none;
  background: none;
  font-family: inherit;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  cursor: pointer;
}
.chart-compare:hover { color: var(--color-text-primary); }
.chart-compare.is-on { color: var(--color-text-primary); }
.chart-compare__dash {
  width: 18px;
  border-top: 2px dashed var(--color-text-tertiary);
}
.chart-compare__switch {
  position: relative;
  width: 30px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--color-border-strong);
  transition: background var(--duration-fast) var(--ease-standard);
}
.chart-compare__switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #FFFFFF;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  transition: transform var(--duration-fast) var(--ease-standard);
}
.chart-compare.is-on .chart-compare__switch { background: var(--color-accent); }
.chart-compare.is-on .chart-compare__switch::after { transform: translateX(12px); }

/* ============================== 6. RESPONSIVE ============================== */

@media (max-width: 960px) {
  .grid--2col { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
  .page { padding: var(--space-5) var(--space-4) var(--space-12); }
  .header { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
  .header__meta { align-items: flex-start; }
  /* minmax(0, 1fr), not plain 1fr: a plain 1fr track can't shrink below its
     content, so tile contents were resizing the two columns. */
  .grid--kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ============================== 7. AUTH (login / access-denied / no-client) ============================== */
/* These replace the whole page (see app.js's replacePageWith()) rather than
   sitting alongside the report, since none of the report's data can load
   until whichever of these states resolves. Kept in their own section
   rather than folded into an existing one since they're new with the
   multi-client login build, not part of the original report layout. */

.auth-message {
  max-width: 420px;
  margin: var(--space-16) auto 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  text-align: center;
}

/* Centred regardless of .auth-message--login overriding text-align to left
   below -- text-align only affects inline content inside a flex item, not
   the item's own position, so this needs its own alignment rule rather than
   inheriting the parent's. */
.auth-message__logo {
  align-self: center;
  height: 36px;
  width: auto;
  max-width: 200px;
  object-fit: contain;
}

.auth-message__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
}

.auth-message__body {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  line-height: 1.5;
}

.auth-message--login {
  text-align: left;
}

/* The heading stays centred under the logo above it even on the "login"
   variant -- only the body copy and form fields below it read better left-
   aligned, which is the whole reason that variant exists. */
.auth-message--login .auth-message__title {
  text-align: center;
}

.login-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.login-form__label {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  margin-top: var(--space-2);
}

.login-form__input {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: inherit;
  font-size: var(--text-sm);
}

.login-form__error {
  font-size: var(--text-sm);
  color: var(--color-negative);
}

/* Quiet text button under the sign-in / reset forms ("Forgot your password?",
   "Back to sign in"). */
.login-form__link {
  align-self: center;
  margin-top: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border: none;
  background: none;
  font-family: inherit;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  cursor: pointer;
}
.login-form__link:hover { color: var(--color-accent-text); text-decoration: underline; }

.btn-primary {
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: none;
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  transition: opacity var(--duration-fast) var(--ease-standard);
}

.btn-primary:hover { opacity: 0.85; }
.btn-primary:disabled { opacity: 0.6; cursor: default; }

/* Home + Log out share one look -- the header's two "leave this dashboard"
   actions, styled identically so they read as a matched pair rather than
   one looking more important than the other. */
.home-button,
.logout-button {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  transition: border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
}

.home-button:hover,
.logout-button:hover {
  border-color: var(--color-border-strong);
  color: var(--color-accent-text);
}

/* Wider than .auth-message (which stays a narrow 420px form/text column) --
   a grid of tiles needs the extra room, and reuses .auth-message__title /
   .auth-message__body directly rather than duplicating those rules. */
.dashboard-picker {
  max-width: 640px;
  margin: var(--space-16) auto 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  text-align: center;
}

.dashboard-picker__grid {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-4);
}

.dashboard-picker__tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-4);
  text-decoration: none;
  color: inherit;
}

.dashboard-picker__tile-logo {
  height: 40px;
  width: auto;
  max-width: 140px;
  object-fit: contain;
}

/* Shown instead of a logo for a client whose Config tab has no Logo URL
   filled in yet -- tinted with their own accent colour (falling back to
   the page's current default) so it still looks intentional, not broken. */
.dashboard-picker__tile-fallback {
  height: 40px;
  width: 40px;
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-weight: var(--weight-semibold);
  font-size: var(--text-lg);
}

.dashboard-picker__tile-name {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--color-text-primary);
}

.dashboard-picker__empty {
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
}
