/* ═══════════════════════════════════════════════════════════════
   THEME TOKENS  — every value maps to a per-game JSON `intro` field
   (primary_color, secondary_color, third_color, fourth_color,
    fifth_color, background_color, font_family, secondary_font_family…)
   Neutrals are DERIVED with color-mix — no new hardcoded palette.
═══════════════════════════════════════════════════════════════ */
:root{
  /* ↓ set by applyConfig(intro) — identical var names & defaults to landing.html */
  --background-color:#06080f; --background-secondary-color:#0c1018; --background-third-color:#101520;
  --primary-color:#00d4ff; --primary-color-rgb:0,212,255;
  --secondary-color:#ff3060; --secondary-color-rgb:255,48,96;
  --third-color:#ffd000; --fourth-color:#dce6f0; --fifth-color:#4e6070; --sixth-color:#000;
  --font-family:'Barlow Condensed'; --secondary-font-family:'Barlow';
  /* Type scale. --fs / --fs2 are the two bases this page is drawn around: --fs is
     the tile name, --fs2 the widget body copy. A config that ships font_size /
     secondary_font_size (rem) replaces them and every other size follows by
     ratio, so client typography drives the page without flattening it — reading
     var(--font-size, 19px) on each rule instead collapses every label to one
     size the moment a client sets a value. The clamp is the guard rail: configs
     ship 2rem-3rem for landing.html, where those values land on a hero headline,
     not on a bento card — unclamped, a module name here reaches 32-48px and
     ROAR CHALLENGE breaks across three lines inside its tile. */
  --fs : clamp(15px, var(--font-size, 19px), 24px);
  --fs2: clamp(11px, var(--secondary-font-size, 13px), 16px);

  /* --font-size / --secondary-font-size are NOT defaulted here on purpose: every
     rule below reads them as var(--font-size, 19px), so an unthemed config keeps
     the type hierarchy this page was drawn with, and a config that ships
     font_size / secondary_font_size overrides it (applyConfig sets them only
     when the JSON carries a value). Defaulting them to 1rem here would make
     every fallback dead and flatten the page to 16px. */

  /* ↓ neutral scaffolding — --ink/--muted set from background luminance so any
     client palette stays legible in this layout; surfaces derive from them. */
  /* One horizontal gutter for the whole page. Section heads used to sit at
     20px while the grid, widget and buttons sat at 16px, so every heading was
     inset 4px from the content it labelled. */
  --gutter: 16px;
  /* Panels (.sports-widget, .partner) share one inset and one radius so their
     left and right edges line up down the page. The banner slider is sized off
     this rather than running full bleed. */
  --panel-r: 14px;
  --page-backdrop:#07080c;   /* replaced from background_color in applyConfig */
  --header-logo-size:3.875rem;   /* replaced from intro.header_logo_size */
  --ink   : #eef4fb;
  --muted : rgba(238,244,251,.55);
  /* TILES — the bento grid carries its own base colour, never the page's.
     applyConfig() replaces it: the page background in the config's native
     mode, the client's background_secondary_color once the fan flips the
     toggle, or intro.tile_background_color when a config names one outright.
     Nothing but the tiles reads it, so the panels, the partner strip and the
     widget keep deriving from --background-color. */
  --tile-background-color : var(--background-color);
  --surface   : color-mix(in srgb, var(--background-color) 90%, var(--secondary-color));
  --surface-2 : color-mix(in srgb, var(--background-color) 84%, var(--secondary-color));
  --surface-3 : color-mix(in srgb, var(--background-color) 78%, var(--secondary-color));
  --hair      : color-mix(in srgb, var(--secondary-color) 12%, transparent);
  --hair-soft : color-mix(in srgb, var(--secondary-color) 7%, transparent);
  --r         : 18px;
  --r-sm      : 12px;
  --tick      : 34px;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* The scrollbar gutter is reserved on every theme: without it a taller
   config (18 modules vs 7) summons a scrollbar, the viewport loses ~15px,
   and near 520px that flips the phone-frame media query on and off — which
   reads as the page changing width when you switch theme. */
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%;scrollbar-gutter:stable}
body{
  font-family:var(--secondary-font-family),system-ui,sans-serif;
  color:var(--secondary-color);
  background:var(--background-color);
  min-height:100vh;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
img{    max-width: 80%;
    display: block;
    margin: 0 auto;}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}

/* Desktop: constrain to a phone frame like the real .mobile-frame */
.frame{
  position:relative;
  width:min(440px,100%);
  max-width:440px;
  margin:0 auto;
  min-height:100vh;
  background:var(--background-color);
  overflow:hidden;
  padding-bottom:120px;
  /* held back until applyConfig has run, so the client's palette is the first
     thing painted rather than a flash of the built-in defaults */
  opacity:0;transition:opacity .32s ease;
}
.frame.ready{opacity:1}
@media(prefers-reduced-motion:reduce){.frame{transition:none}}
@media(min-width:520px){
  body{
    background-color: var(--background-color, #06080f) !important;
    background-image: var(--background-image, none);
    background-size: cover !important;
    background-position: center top !important;
    background-attachment: fixed !important;
    background-repeat: no-repeat !important;
  }
.frame{
    border-radius:34px;
    box-shadow:0 0 0 1px var(--hair), 0 40px 120px rgba(0,0,0,.7);
    min-height:calc(100vh - 44px);
  }
}

/* Ambient field — one soft wash from the theme accents, not orbs-soup */
/* absolute, not fixed: inside a 440px frame a fixed wash spills across the
   whole desktop viewport instead of sitting behind the page. */
.field{position:absolute;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(120% 60% at 80% -8%, rgba(var(--primary-color-rgb),.16), transparent 60%),
    radial-gradient(90% 50% at -10% 12%, rgba(var(--secondary-color-rgb),.12), transparent 55%);
}
.frame > *{position:relative;z-index:1}

/* ═══════════════════ TOP BAR ═══════════════════ */
.bar{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:16px 18px 12px;
}
/* PageLang.renderSwitcher leaves #switcher empty below two languages — with
   nothing on the right there is nothing to space against, so the logo centres
   rather than hanging off the left edge. */
.bar:has(.bar-tools:empty){justify-content:center}
/* one language and no toggle leaves an empty tools block — collapse it so the
   logo centres instead of balancing against nothing */
.bar:has(#switcher:empty):not(:has(.theme-btn)){justify-content:center}
.bar-logo{display:flex;align-items:left;gap:9px;min-width:0}
/* height comes from intro.header_logo_size (rem), the same way the QR takes
   intro.qr_font_size; 3.875rem is the previous fixed 62px. */
.bar-logo .logo-img{/*height:var(--header-logo-size,3.875rem);*/width:auto;max-width:100%;max-height: 15vh;object-fit:contain;display:none;
  filter:drop-shadow(0 2px 8px rgba(0,0,0,.45))}
/* the logo owns the bar's height, so the bar has to grow with it rather than
   the image being squeezed back down by a fixed cap.
   flex:1 1 auto + centred content: the block claims everything the tools do
   not, so the logo sits in the middle of THAT space rather than pinned to the
   left edge. Centring the whole bar instead would push the logo under the
   flags and the toggle on a wide mark. min-width:0 above keeps it shrinkable. */
.bar-logo{flex:1 1 auto;justify-content:center}
.bar-logo .mark{
  width:52px;height:52px;border-radius:13px;flex:0 0 auto;
  display:grid;place-items:center;color:var(--background-color);
  background:linear-gradient(135deg,var(--primary-color),var(--secondary-color));
  font-family:var(--font-family);font-weight:900;font-size:calc(var(--fs) * 1.368);
}
/* the club name is already in the logo — the text duplicate is dropped */
.bar-logo .club{display:none;font-family:var(--font-family);font-weight:800;font-size:calc(var(--fs) * 0.842);
  letter-spacing:.02em;text-transform:uppercase;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar-logo .club small{display:block;font-family:var(--secondary-font-family);
  font-weight:500;font-size:calc(var(--fs2) * 0.808);letter-spacing:.14em;color:var(--muted);margin-top:3px;text-transform:uppercase}
.bar-tools{display:flex;align-items:center;gap:8px;flex:0 0 auto}
/* language switcher — same markup + /assets/img/{lang}
.png flags as
   landing.html, rendered by PageLang.renderSwitcher(). Ported from
   landingd.css; only the flag size is scaled down to sit in the top bar. */
.lang-switcher{list-style:none;margin:0;padding:0;display:flex;align-items:center;justify-content:center;gap:6px}
/* corner-shape only reshapes a corner that a radius already rounds, and on a
   replaced element it needs the box to actually clip — an <img> rounds its own
   content but does not apply the superellipse geometry without overflow:hidden.
   The <a> carries the same shape so the border ring is clipped too. Browsers
   without corner-shape fall back to the plain 50% circle. */
.lang-switcher li a{display:block;line-height:0;overflow:hidden;
  border-radius:50%;corner-shape:squircle}
.lang-switcher li{flex:0 0 auto;list-style:none;margin:0;display:inline-flex}
/* the page-wide `img{max-width:80%}` above also hits the flags: the percentage
   resolves against the <li> the flag itself sizes, so a 36px flag came back
   ~29px wide and rendered as a squashed oval. brackets.html never showed it
   because lang-switcher.css ships no such global rule. */
.lang-switcher li img{width:36px;height:36px;max-width:none;margin:0;display:block;object-fit:cover;
  border-radius:50%;corner-shape:squircle;overflow:hidden;
  transition:opacity .2s,transform .2s;cursor:pointer;
  border:5px solid color-mix(in srgb,var(--primary-color) 55%,transparent);opacity:.55}
/* In light mode primary_color is #ffffff on most configs — an invisible ring on
   a near-white page. The secondary is the colour that still reads there. */
:root[data-theme="light"] .lang-switcher li img{
  border-color:color-mix(in srgb,var(--secondary-color) 60%,transparent)}
:root[data-theme="light"] .lang-switcher li a.active img{border-color:var(--secondary-color)}
.lang-switcher li a.active img{border-color:var(--primary-color);opacity:1}
.lang-switcher li img:hover{opacity:1;transform:scale(1.12)}

/* light / dark toggle — the icon shows the mode you would switch TO */
.theme-btn{width:44px;height:44px;flex:0 0 auto;display:grid;place-items:center;
  border-radius:50%;corner-shape:squircle;cursor:pointer;
  background:color-mix(in srgb,var(--secondary-color) 7%,transparent);
  border:1px solid color-mix(in srgb,var(--secondary-color) 14%,transparent);
  color:var(--secondary-color);transition:background .18s,border-color .18s,transform .12s;display:none}
.theme-btn:hover{border-color:color-mix(in srgb,var(--primary-color) 55%,transparent)}
.theme-btn:active{transform:scale(.94)}
.theme-btn svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.9;
  stroke-linecap:round;stroke-linejoin:round;grid-area:1/1}
:root[data-theme="dark"]  .theme-btn .ic-moon{display:none}
:root[data-theme="light"] .theme-btn .ic-sun {display:none}

/* ═══════════════════ SPORTS WIDGET ═══════════════════
   Follows the live landing2162 panel: a light translucent card floating on
   the page background, not an opaque slab — blurred backdrop, hairline
   border, and colour carried by the theme (scores and kickoff times in
   --primary-color, live in --secondary-color) rather than by chrome.
   Neutrals still derive from --ink so a light client background works. */
.sports-widget{
  margin:16px var(--gutter) 0;border-radius:var(--panel-r);overflow:hidden;
  /* A neutral gray sheet, not a tint of the page. At 7% ink the panel was
     effectively transparent, so scores and flags competed with whatever the
     client's key visual was doing underneath — and that changes per game.
     Gray gives the type one predictable surface everywhere. Kept slightly
     translucent over the blur so the panel still reads as sitting ON the page
     rather than punched out of it. */
  background:rgba(28,29,33,.86);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
  border:1px solid color-mix(in srgb,var(--secondary-color) 11%,transparent);
  font-family:var(--secondary-font-family),system-ui,sans-serif;
  /* the widget does not inherit the client's type scale — intro.font_size can
     be 2.5rem, which blew every label and score up with it */
  font-size:1rem;line-height:1.25;
}
.sports-widget[hidden]{display:none}
/* Light mode flips --ink dark, so a dark gray sheet would put dark type on a
   dark panel. The gray follows the mode; both stay neutral. */
:root[data-theme="light"] .sports-widget,
:root[data-theme="light"] .now-slot{background:rgba(242,243,246,.9)}

/* ── NOW SLOT ──────────────────────────────────────────────────────────
   The band is the entry point to the results panel further down: it is an
   anchor, so it reads and behaves as something you can act on. */
.now-slot{display:block;margin:14px var(--gutter) 0;border-radius:14px;overflow:hidden;
  /* same gray as the results panel — the accent band fills most of this box,
     but the country picker strip under it sits on the bare surface */
  position:relative;background:rgba(28,29,33,.86);
  border:1px solid color-mix(in srgb,var(--secondary-color) 11%,transparent)}
.now-slot[hidden]{display:none}
.now-link{display:block;text-decoration:none;color:inherit}
.now-link[hidden]{display:none}
.now-link:active{transform:scale(.995)}
.now-slot .sw-now{border-radius:0}
/* the country picker sits under the fixture, on the page surface rather than
   on the accent band, so the two read as separate controls */
.now-slot .fc-bar{border-bottom:none!important;padding:7px 10px!important;margin:0!important;
  background:transparent!important;display:flex;gap:6px;align-items:center;
  overflow-x:auto;scrollbar-width:none}
.now-slot .fc-bar::-webkit-scrollbar{display:none}
.now-link + .fc-bar{border-top:1px solid color-mix(in srgb,var(--secondary-color) 11%,transparent)}
.now-cue{display:flex;align-items:center;justify-content:center;gap:6px;
  padding:7px 10px;font-family:var(--secondary-font-family),sans-serif;
  font-weight:700;font-size:1rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--on-accent);
  background:color-mix(in srgb,var(--accent) 82%, var(--background-color));
  box-shadow:inset 0 1px 0 color-mix(in srgb,var(--on-accent) 18%,transparent)}
.now-cue svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2.4;
  stroke-linecap:round;stroke-linejoin:round;animation:cue-bob 1.8s ease-in-out infinite}
@keyframes cue-bob{0%,100%{transform:translateY(-1px)}50%{transform:translateY(2px)}}
@media(prefers-reduced-motion:reduce){.now-cue svg{animation:none}}
/* the panel it jumps to, marked so the landing is obvious */
.sports-widget:target{outline:2px solid var(--accent);outline-offset:3px}

/* ═══ ARRIVAL ═══
   The tiles already reveal on scroll; the furniture above them appeared fully
   formed, so the page looked half-animated. One short staggered settle, and
   only on first paint — .ready is set once. */
@keyframes settleIn{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}
.frame.ready .bar      {animation:settleIn .5s cubic-bezier(.2,.75,.2,1) both}
.frame.ready .now-slot {animation:settleIn .5s cubic-bezier(.2,.75,.2,1) .07s both}
.frame.ready .sec      {animation:settleIn .5s cubic-bezier(.2,.75,.2,1) .14s both}
@media(prefers-reduced-motion:reduce){
  .frame.ready .bar,.frame.ready .now-slot,.frame.ready .sec{animation:none}
}

/* ═══ THE BAND WHEN IT IS LIVE ═══
   Up next is a quiet card; live is the thing the fan is standing in front of.
   The band stops being flat: a specular sweep crosses it, the edge breathes,
   and the dot throws a radar ring instead of only blinking. Nothing moves
   fast — it should read as "on air", not as a notification. */
@keyframes nowSweep{0%{transform:translateX(-160%) skewX(-14deg)}
                    60%,100%{transform:translateX(320%) skewX(-14deg)}}
@keyframes nowBreathe{0%,100%{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--on-accent) 16%,transparent)}
                      50%{box-shadow:inset 0 0 0 2px color-mix(in srgb,var(--on-accent) 46%,transparent)}}
@keyframes nowRadar{0%{transform:scale(1);opacity:.55}
                    100%{transform:scale(3.4);opacity:0}}
@keyframes scorePop{0%{transform:scale(1)}38%{transform:scale(1.22)}100%{transform:scale(1)}}

.sw-now.is-live{position:relative;overflow:hidden;animation:nowBreathe 3.2s ease-in-out infinite}
.sw-now.is-live::after{content:"";position:absolute;z-index:1;pointer-events:none;
  top:-40%;bottom:-40%;left:0;width:22%;
  background:linear-gradient(90deg,transparent,
    color-mix(in srgb,var(--on-accent) 22%,transparent),transparent);
  filter:blur(4px);
  animation:nowSweep 5.5s ease-in-out infinite}
.sw-now.is-live > *{position:relative;z-index:2}
/* the dot keeps its steady blink and gains a ring pushing out from under it */
.sw-now.is-live .sw-now-label.live{position:relative}
.sw-now.is-live .sw-now-label.live::after{content:"";position:absolute;left:0;top:50%;
  width:7px;height:7px;margin-top:-3.5px;border-radius:50%;
  background:currentColor;pointer-events:none;
  animation:nowRadar 2.4s ease-out infinite}
/* the score reacts the moment it changes, not on a loop */
.sw-now-score.bumped{animation:scorePop .5s cubic-bezier(.2,.9,.3,1)}

@media(prefers-reduced-motion:reduce){
  .sw-now.is-live,.sw-now.is-live::after,
  .sw-now.is-live .sw-now-label.live::after,.sw-now-score.bumped{animation:none}
}

/* ── NOW / NEXT ──────────────────────────────────────────────────────
   The one row a fan needs, pinned above the tabs. A soft accent wash, no
   texture — it should read as part of the same light panel. */
/* The Now strip is the answer a fan in the stand actually wants, so it is a
   filled accent band rather than a 9% tint — everything inside it inverts to
   --on-accent, which applyConfig already derives from the accent's luminance,
   so it holds on a lime, navy or white client accent alike. */
.sw-now{position:relative;padding:14px 14px 15px;
  background:linear-gradient(135deg,
    var(--accent),
    color-mix(in srgb,var(--accent) 82%, var(--background-color)));
  color:var(--on-accent);
  box-shadow:inset 0 -1px 0 color-mix(in srgb,var(--on-accent) 18%,transparent)}
.sw-now[hidden]{display:none}
.sw-now-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:11px}
.sw-now-label{font-family:var(--secondary-font-family),sans-serif;font-weight:800;
  font-size:1rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--on-accent)}
.sw-now-label.live::before{content:'';display:inline-block;width:7px;height:7px;border-radius:50%;
  background:currentColor;margin-right:7px;vertical-align:middle;
  animation:blink 1.1s ease-in-out infinite}
.sw-now-go{margin-left:8px;flex:0 0 auto;font-family:var(--secondary-font-family),sans-serif;
  font-weight:800;font-size:1rem;letter-spacing:.16em;text-transform:uppercase;
  color:var(--on-accent);opacity:.8}
.sw-now-go::after{content:' \2197'}
.sw-now[data-href]:not([data-href=""]){cursor:pointer}
/* a linked game row reads as tappable */
.sw-game--link{cursor:pointer;text-decoration:none;color:inherit;display:grid}
.sw-game--link:hover{background:color-mix(in srgb,var(--secondary-color) 8%,transparent)}
.sw-now-meta{font-size:1rem;letter-spacing:.1em;text-transform:uppercase;
  color:color-mix(in srgb,var(--on-accent) 68%,transparent);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sw-now-teams{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:12px}
/* Both sides stack the same way — flag over name, centred — so the two teams
   read as a symmetric pair instead of mirroring each other with row-reverse. */
.sw-now-side{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:8px;min-width:0;text-align:center}
.sw-now-side.away{flex-direction:column;text-align:center}
.sw-now-side img{width:42px;height:31px;object-fit:cover;flex-shrink:0;border-radius:4px}
.sw-now-side span{max-width:100%;font-family:var(--secondary-font-family),sans-serif;font-weight:800;
  font-size:1rem;
  line-height:1.02;text-transform:uppercase;color:var(--on-accent);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sw-now-mid{display:flex;flex-direction:column;align-items:center;gap:3px;flex-shrink:0}
.sw-now-time,.sw-now-score{font-family:var(--secondary-font-family),sans-serif;font-weight:900;
  font-variant-numeric:tabular-nums;line-height:1;color:var(--on-accent)}
.sw-now-time{font-size:1rem}
.sw-now-score{font-size:1rem;display:flex;align-items:center;gap:6px}
.sw-now-score i{font-style:normal;font-weight:300;font-size:1rem;
  color:color-mix(in srgb,var(--on-accent) 45%,transparent)}
.sw-now-cd{display:flex;align-items:center;gap:5px;font-size:1rem;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;
  color:color-mix(in srgb,var(--on-accent) 72%,transparent)}
.sw-now-cd > [data-i18n]:empty{display:none}
/* countdown cells — small, flat, tabular. Deliberately not a flip clock: the
   digits change once a minute and a card-flip would draw the eye off the
   fixture it is describing. */
.cd-cell{display:inline-flex;align-items:baseline;gap:2px;
  padding:2px 6px;border-radius:6px;
  background:color-mix(in srgb,var(--on-accent) 12%,transparent)}
.cd-cell b{font-family:var(--secondary-font-family),sans-serif;font-weight:900;
  font-size:1rem;line-height:1;font-variant-numeric:tabular-nums;
  color:var(--on-accent)}
.cd-cell i{font-style:normal;font-size:1rem;font-weight:700;opacity:.7}

/* ── TABS ── */
.sw-tabs{display:flex;gap:2px;padding:3px;background:color-mix(in srgb,var(--secondary-color) 10%,transparent);
  overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.sw-tabs::-webkit-scrollbar{display:none}
.sw-tab[hidden]{display:none}
.sw-tab{flex:0 0 auto;padding:6px 8px;border:none;border-radius:7px;background:transparent;
  color:var(--muted);font-family:inherit;font-weight:600;
  text-transform:uppercase;letter-spacing:.03em;white-space:nowrap;cursor:pointer;transition:all .2s}
.sw-tab:hover{color:var(--secondary-color)}
.sw-tab.active{background:color-mix(in srgb,var(--secondary-color) 13%,transparent);color:var(--secondary-color)}

/* ── ONE CONTROL ROW ──
   Day chips, category chips and the country picker used to be three stacked
   bands above the data — roughly 120px of chrome before a single result.
   They are one scrolling row now: #fc-bar is moved inside #sw-filters after
   FanCountry mounts (see mergeWidgetControls), and nothing wraps to a second
   line, so the row is a fixed ~34px whatever a config switches on. */
.sw-filters{display:flex;align-items:center;gap:6px;padding:5px 8px;
  overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;
  border-bottom:1px solid color-mix(in srgb,var(--secondary-color) 8%,transparent)}
.sw-filters::-webkit-scrollbar{display:none}
.sw-filters[hidden]{display:none}
.sw-chips{display:flex;gap:4px;flex:0 0 auto}
/* category (Men/Women) chips are hidden — the split already shows in the pool
   headers. NB the bracket used these to pick which tree to draw, so it now
   always shows the first category. Delete this rule to bring them back. */
.sw-chips--cat{display:none}
.sw-chips+.sw-chips{padding-left:6px;
  border-left:1px solid color-mix(in srgb,var(--secondary-color) 12%,transparent)}
.sw-chip{flex:0 0 auto;padding:4px 9px;border-radius:999px;cursor:pointer;
  border:1px solid color-mix(in srgb,var(--secondary-color) 12%,transparent);background:transparent;
  font-family:inherit;font-weight:600;font-size:1rem;letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);white-space:nowrap;transition:all .2s}
.sw-chip:hover{color:var(--secondary-color)}
.sw-chip.active{background:color-mix(in srgb,var(--primary-color) 88%,transparent);
  border-color:transparent;color:var(--background-color)}
/* the country bar joins the same row instead of owning its own band */
/* "On top" is hidden: it and the country filter are two ways of doing the
   same thing, and the row has no space for both. Styled here rather than in
   fan-country.js, which the other landing pages share. */
#fc-bar .fc-btn[data-fc="pin"]{display:none}

.sw-content{padding:6px;min-height:64px}
.sw-loading{display:flex;align-items:center;justify-content:center;gap:8px;
  padding:20px 0;color:var(--muted)}
.sw-loading::after{content:'';width:14px;height:14px;border-radius:50%;
  border:2px solid color-mix(in srgb,var(--secondary-color) 15%,transparent);
  border-top-color:var(--primary-color);animation:spin .7s linear infinite}
.sw-empty{text-align:center;padding:16px 8px;color:var(--muted);font-size:1rem}

/* ── GAME ROWS ── */
.sw-game{display:grid;grid-template-columns:1fr 62px 1fr;align-items:center;gap:6px;
  padding:7px 8px;border-radius:8px;transition:background .15s;font-variant-numeric:tabular-nums}
.sw-game+.sw-game{margin-top:2px}
.sw-game:hover{background:color-mix(in srgb,var(--secondary-color) 5%,transparent)}
/* the fixture pinned in the Now strip, marked where it sits in the list */
.sw-game--now{background:color-mix(in srgb,var(--primary-color) 12%,transparent);
  box-shadow:inset 2px 0 0 var(--primary-color)}
.sw-game--now .sw-team .name{color:var(--secondary-color);font-weight:700}
.sw-game--upcoming .sw-team .name{color:color-mix(in srgb,var(--secondary-color) 78%,transparent)}
.sw-team{display:flex;align-items:center;gap:7px;min-width:0}
.sw-team.away{flex-direction:row-reverse;text-align:right}
.sw-team img{width:26px;height:19px;object-fit:cover;flex-shrink:0;border-radius:2px;
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--secondary-color) 14%,transparent)}
.sw-team .name{font-weight:600;font-size:1rem;color:var(--secondary-color);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.sw-center{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;min-width:0}
.sw-vs{font-size:1rem;color:var(--muted);white-space:nowrap}
.sw-status{font-weight:700;font-size:1rem;letter-spacing:.04em;text-transform:uppercase;
  line-height:1;text-align:center}
.sw-status .sw-date{display:block;font-size:1rem;font-weight:600;opacity:.6;line-height:1.2}
.sw-status.live{color:#22c55e}
.sw-status.finished{color:var(--muted)}
.sw-status.scheduled{color:var(--primary-color)}
.sw-scores{display:flex;align-items:center;gap:4px;font-weight:700;font-size:1rem;
  font-variant-numeric:tabular-nums;color:var(--secondary-color)}
.sw-scores .sep{color:color-mix(in srgb,var(--secondary-color) 25%,transparent);font-weight:300;font-size:1rem}

/* ── STANDINGS ──
   The widget ships a <colgroup> with table-layout:fixed (rank 24px, then
   28/28/28/36), so the table is inset with a margin rather than per-cell
   padding — padding inside a 24px rank column would crush the number. */
.sw-standings{width:calc(100% - 12px);margin:0 6px;border-collapse:collapse;
  font-variant-numeric:tabular-nums}
.sw-standings th{padding:6px 4px;font-weight:700;font-size:1rem;letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted);text-align:center;
  border-bottom:1px solid color-mix(in srgb,var(--secondary-color) 8%,transparent)}
.sw-standings th:nth-child(2){text-align:left;padding-left:8px}
.sw-standings td{padding:6px 4px;text-align:center;font-size:1rem;
  color:color-mix(in srgb,var(--secondary-color) 80%,transparent)}
.sw-standings td:nth-child(2){text-align:left;padding-left:8px}
.sw-standings tbody tr+tr td{border-top:1px solid color-mix(in srgb,var(--secondary-color) 6%,transparent)}
.sw-standings .sw-rank{font-weight:700;color:color-mix(in srgb,var(--secondary-color) 42%,transparent);
  padding-left:0;padding-right:0}
.sw-standings tbody tr:nth-child(-n+3) .sw-rank{color:var(--primary-color)}
.sw-standings .sw-team-cell{display:flex;align-items:center;gap:6px}
.sw-standings .sw-team-cell img{width:22px;height:16px;object-fit:cover;flex-shrink:0;border-radius:2px;
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--secondary-color) 12%,transparent)}
.sw-standings .sw-team-cell .name{font-weight:600;white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;max-width:120px}
.sw-standings .sw-pts{font-weight:700;color:var(--secondary-color)}

.sw-pool-header{font-weight:700;font-size:1rem;letter-spacing:.09em;text-transform:uppercase;
  color:var(--muted);padding:10px 8px 4px;
  border-bottom:1px solid color-mix(in srgb,var(--secondary-color) 11%,transparent);margin-bottom:2px}
.sw-pool-header:not(:first-child){margin-top:10px}

/* ── F1 RACE LIST ── */
.sw-race{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:8px}
.sw-race+.sw-race{margin-top:2px}
.sw-race:hover{background:color-mix(in srgb,var(--secondary-color) 5%,transparent)}
.sw-race-round{font-weight:700;font-size:1rem;color:color-mix(in srgb,var(--secondary-color) 38%,transparent);
  min-width:24px;text-align:center}
.sw-race-name{flex:1;min-width:0;font-weight:600;font-size:1rem;color:var(--secondary-color);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sw-race-date{font-weight:600;font-size:1rem;color:var(--muted);white-space:nowrap}

/* ── KNOCKOUT TREE ───────────────────────────────────────────────────
   Rounds are flex columns; space-around puts each later round's cards on
   the midpoints of the pair that feeds them. Connectors are an SVG overlay
   measured after paint (drawBracketLines), so they hold at any width. */
.sw-bracket-wrap{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;
  -webkit-overflow-scrolling:touch;padding:10px 10px 12px;
  -webkit-mask:linear-gradient(90deg,#000 0,#000 calc(100% - 16px),transparent 100%)}
.sw-bracket-wrap::-webkit-scrollbar{display:none}
.sw-bracket{position:relative;display:flex;gap:26px;min-width:min-content}
.sw-br-lines{position:absolute;inset:0;pointer-events:none;overflow:visible}
.sw-br-lines path{fill:none;stroke:color-mix(in srgb,var(--secondary-color) 20%,transparent);stroke-width:1.5}
.sw-br-round{display:flex;flex-direction:column;flex:0 0 112px}
.sw-br-head{font-weight:700;font-size:1rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted);text-align:center;padding-bottom:8px;margin-bottom:2px;
  border-bottom:1px solid color-mix(in srgb,var(--secondary-color) 10%,transparent)}
/* the column that holds the cards — space-around is what makes the rounds
   line up, so nothing else may set its distribution */
.sw-br-col{flex:1;display:flex;flex-direction:column;justify-content:space-around}
.sw-br-match{position:relative;border-radius:8px;overflow:hidden;
  border:1px solid color-mix(in srgb,var(--secondary-color) 10%,transparent);
  background:color-mix(in srgb,var(--secondary-color) 5%,transparent);transition:all .2s}
.sw-br-match:hover{border-color:color-mix(in srgb,var(--primary-color) 50%,transparent)}
.sw-br-match.pending{background:transparent}
.sw-br-when{font-weight:700;font-size:1rem;letter-spacing:.06em;color:var(--primary-color);
  text-align:center;padding:4px 0 3px;
  border-bottom:1px solid color-mix(in srgb,var(--secondary-color) 8%,transparent)}
.sw-br-side{display:flex;align-items:center;gap:6px;padding:5px 7px;font-variant-numeric:tabular-nums}
.sw-br-side+.sw-br-side{border-top:1px solid color-mix(in srgb,var(--secondary-color) 6%,transparent)}
.sw-br-side img{width:20px;height:15px;object-fit:cover;flex-shrink:0;border-radius:2px;
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--secondary-color) 14%,transparent)}
.sw-br-noflag{width:20px;height:15px;flex-shrink:0;border-radius:2px;
  background:color-mix(in srgb,var(--secondary-color) 9%,transparent)}
.sw-br-code{flex:1;font-weight:700;font-size:1rem;letter-spacing:.03em;color:var(--muted)}
.sw-br-pts{font-weight:700;font-size:1rem;color:var(--muted)}
/* the winner is the only thing in the tree that gets full ink */
.sw-br-side.won .sw-br-code{color:var(--secondary-color)}
.sw-br-side.won .sw-br-pts{color:var(--primary-color)}
.sw-br-side.won{background:color-mix(in srgb,var(--primary-color) 12%,transparent)}

.sw-more{display:block;width:100%;min-height:34px;text-align:center;margin-top:6px;
  font-family:inherit;font-weight:700;font-size:1rem;letter-spacing:.03em;color:var(--muted);
  background:color-mix(in srgb,var(--secondary-color) 8%,transparent);border:none;
  border-top:1px solid color-mix(in srgb,var(--secondary-color) 9%,transparent);
  cursor:pointer;transition:all .2s;border-radius:0 0 10px 10px}
.sw-more:hover{background:color-mix(in srgb,var(--secondary-color) 13%,transparent);color:var(--secondary-color)}
.sw-more::after{content:' \25BE';margin-left:4px}
.sw-more.expanded::after{content:' \25B4'}

/* ═══════════════════ SECTION HEAD + FILTERS ═══════════════════ */
.sec{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:26px var(--gutter) 12px}
.sec h2{min-width:0;line-height:1.06;text-wrap:balance}
.sec h2:empty::after{content:'Fan Zone'}
.sec .count{flex:0 0 auto;padding-top:4px;white-space:nowrap}
.sec h2{font-family:var(--font-family);font-weight:900;font-size:calc(var(--fs));letter-spacing:.02em;text-transform:uppercase;color:var(--primary-color)}
.sec .count{font-family:var(--font-family);font-weight:700;font-size:calc(var(--fs) * 0.684);letter-spacing:.04em;color:var(--primary-color)}
.sec .count b{color:var(--primary-color)}

/* ═══════════════════ BENTO GAME GRID ═══════════════════ */
.grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:0 var(--gutter)}
.tile{grid-column:span 1;position:relative;text-decoration:none;color:inherit;
  border-radius:var(--r);overflow:hidden;background:var(--tile-background-color);cursor:pointer;isolation:isolate;
  min-height:132px;display:flex;flex-direction:row;align-items:center;gap:8px;
  transition:transform .14s, box-shadow .25s;
  opacity:0;transform:translateY(18px) scale(.982)}
.tile.in{opacity:1;transform:none;transition:transform .62s cubic-bezier(.2,.75,.2,1),opacity .5s}
.tile:hover{box-shadow:0 20px 46px -26px rgba(0,0,0,.9)}
.tile:hover .name{transform:translateX(3px)}
.tile:active{transform:scale(.985)}
.tile.wide{grid-column:span 2;min-height:126px}
.tile.feat{grid-column:span 2;min-height:190px}
/* a single-module config gets a full-bleed card rather than one tile adrift
   in a two-column grid */
.grid:has(.tile:only-child) .tile{min-height:260px}
.grid:has(.tile:only-child) .tile .card-picto{width:7rem;min-width:7rem;height:7rem;min-height:7rem}

/* ART — the client's own key visual (intro.mobile_image || background_image),
   sliced across the grid: paintTileArt() gives every tile the offset of its
   own rectangle, so the tiles together reassemble the poster and the gutters
   read as tears in it. A module that ships its own image uses that instead,
   and with no artwork at all the accent gradient underneath still carries. */
.tile .art{position:absolute;inset:0;z-index:-1;
  background-image:linear-gradient(140deg, var(--a1), var(--a2))}
.tile.art-poster .art{background-repeat:no-repeat}
.tile .art::before{content:"";position:absolute;inset:0;
  background:radial-gradient(85% 115% at 86% -6%, rgba(255,255,255,.12), transparent 52%)}
/* the scrim runs left-to-right now, because the name does */
.tile .art::after{content:"";position:absolute;inset:0;
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--tile-background-color) 92%, transparent) 0%,
    color-mix(in srgb,var(--tile-background-color) 62%, transparent) 46%,
    color-mix(in srgb,var(--tile-background-color) 18%, transparent) 100%)}
/* max-width:none / margin:0 — the global `img{max-width:80%;margin:0 auto}`
   reset at the top of this sheet applies here too, and it capped every tile
   photo at 80% of its .art and centred it: the artwork sat as a floating
   panel with the tile's own background showing down both sides instead of
   covering the card. Every other full-bleed image in this sheet opts out the
   same way (.logo-img, .scan-logo, .partner-slide img). */
.tile.photo .art img{position:absolute;inset:0;width:100%;height:100%;max-width:none;margin:0;object-fit:cover;filter:saturate(1.05)}
.tile.photo .art::before{background:none}
.tile.art-poster .art::before{background:none}

/* module picto — the real /assets/icons/*.svg used by landing.html, same
   .card-picto contract (they theme themselves off --primary-color /
   --secondary-color, so they re-tint with the client config for free) */
/* the picto is IN FLOW, and .body takes the leftover space below it, so the
   two can never sit on top of each other however long a module name runs */
/* picto sits on the right, name on the left — they are siblings in a row, so
   they cannot overlap however long a module name runs */
.tile .ic{position:relative;z-index:2;flex:0 0 auto;padding-right:12px;margin-left:auto;
  opacity:.92;display:flex;align-items:center}
.card-picto{width:5rem;min-width:5rem;height:5rem;min-height:5rem;
  display:flex;flex-shrink:0;align-items:center;justify-content:center;isolation:isolate;
  filter:drop-shadow(0 4px 14px rgba(0,0,0,.45))}
.card-picto svg{width:100%;height:100%;display:block;overflow:visible}
.tile.feat .card-picto{width:8rem;min-width:8rem;height:8rem;min-height:8rem}
.tile:not(.feat):not(.wide) .card-picto{width:4.25rem;min-width:4.25rem;height:4.25rem;min-height:4.25rem}
/* .card-picto carries min-width/min-height — the sponsor chip is 34px, so it
   has to override both or the picto blows the card out. */
.scard .card-picto{width:34px;min-width:34px;height:34px;min-height:34px;filter:none}
.tile .body{position:relative;z-index:2;flex:1 1 auto;min-width:0;
  padding:12px 4px 12px 14px;display:flex;flex-direction:column;align-items:flex-start;gap:7px}
.tile .name{transition:transform .18s ease;font-family:var(--font-family);font-weight:900;
  font-size:var(--fs);line-height:.98;
  letter-spacing:.01em;text-transform:uppercase;text-shadow:0 2px 12px rgba(0,0,0,.4);
  /* Module names come from the organiser: "ROAR CHALLENGE" has to wrap inside
     the card rather than run under its rounded edge — but at the SPACE. It was
     overflow-wrap:anywhere, which breaks mid-word (STANDIN/GS). break-word only
     splits a single word that cannot fit on a line by itself, which no real
     module name does. */
  overflow-wrap:break-word;word-break:normal;hyphens:none;text-wrap:balance}
.tile.feat .name{font-size:calc(var(--fs))}
  .tile.feat .body{padding:16px 4px 16px 16px;gap:10px}

/* ── NOTICE (item.notice) ──────────────────────────────────────────────
   The organiser's one line about the module: what it is, or when it opens.
   Sits directly under the name in the secondary face, clamped so a long note
   can never push the picto out or blow the tile height. It is a caption, not
   a paragraph: two lines on a square tile, three on the feature card. */
.tile .note{text-transform:uppercase;font-family:var(--secondary-font-family);font-weight:600;
  font-size:calc(var(--fs) * 0.5);line-height:1.28;letter-spacing:.005em;
  color:color-mix(in srgb,var(--secondary-color) 84%,transparent);
  text-shadow:0 1px 9px rgba(0,0,0,.5);
  margin-top:-2px;max-width:32ch;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  line-clamp:2;overflow:hidden}
.tile.feat .note{font-size:calc(var(--fs) * 0.61);-webkit-line-clamp:3;line-clamp:3;max-width:38ch}
.tile:not(.feat):not(.wide) .note{font-size:calc(var(--fs) * 0.47);max-width:22ch}
/* held-back tiles keep the notice at full read — it is where "opens at
   half-time" lives, so it must not grey out with the rest of the card */
.tile.soon .note,.tile.closed .note{color:var(--muted);text-decoration:none}
/* a notice is a second line of type: give the name room to breathe above it */
.tile:has(.note) .name{margin-bottom:1px}

/* ── STOCK MODULE PHOTO ────────────────────────────────────────────────
   MODULE_STYLES.photo — a real shot of the module on a venue screen, used
   when the playlist item ships no artwork. The framing is known (screen
   content centre-right), so the crop is positioned rather than centred, and
   the scrim is pulled back from the default: the whole point is that the fan
   SEES the selfie and the message on the jumbotron. Client-supplied images
   keep the neutral centred crop and the stronger scrim. */
.tile.stock .art img{object-position:50% 45%}
/* This scrim is the tile's ONLY wash now, and it covers the whole tile: .art is
   inset:0, so ::after is too. It has to live on ::after and not ::before — .art
   sets no z-index on its children, so they paint in tree order (::before, then
   the <img>, then ::after) and anything on ::before ends up under the
   photograph (which is why .tile.photo .art::before is blanked above). */
.tile.stock .art::after{
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--tile-background-color) 84%, transparent) 0%,
    color-mix(in srgb,var(--tile-background-color) 38%, transparent) 48%,
    /* plain `transparent`, never color-mix(... 0%, transparent): the minifier
       drops the % off a lone 0, and `color-mix(in srgb,X 0,transparent)` is
       invalid — that killed this whole background in landingb.min.css, which
       is why the tile showed no gradient at all. */
    transparent 100%)}
/* the picto would sit on top of the photo it is meant to stand in for —
   shrink it to a corner mark so the photograph carries the tile */
.tile.stock .ic{align-self:flex-start;padding-top:10px;opacity:.7}
.tile.stock .card-picto{filter:drop-shadow(0 3px 10px rgba(0,0,0,.7))}
.tile.stock:not(.feat) .card-picto{width:2.6rem;min-width:2.6rem;height:2.6rem;min-height:2.6rem}
.tile.stock.feat .card-picto{width:4rem;min-width:4rem;height:4rem;min-height:4rem}
/* the name and the notice sit ON the photograph now — a soft plate in the
   tile's own colour keeps them legible against a bright frame of the mosaic,
   in either theme, without darkening the half of the photo nobody is reading
   over. Derived from --tile-background-color, so it follows the client.
   The plate is folded into the .art::after scrim above and is NOT a background
   on .body: .body is a flex child sized by its own text, so a background there
   painted a rounded band floating ~22px clear of the tile's top and bottom
   edges rather than a plate covering the tile. */


.badge{display:inline-flex;align-items:center;gap:5px;font-family:var(--font-family);font-weight:800;
  font-size:calc(var(--fs) * 0.553);letter-spacing:.1em;text-transform:uppercase;padding:4px 9px;border-radius:7px;margin-bottom:9px}
.badge.live{color:var(--on-secondary,#fff);background:var(--secondary-color)}
.badge.live .d{width:6px;height:6px;border-radius:50%;background:var(--on-secondary,#fff);animation:blink 1.1s infinite}
.badge.open{color:var(--on-third,#0d1016);background:var(--third-color)}
.badge.soon{color:var(--secondary-color);background:transparent;
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--secondary-color) 34%,transparent);
  border:1px solid var(--hair)}
/* ── NOT OPEN YET ──────────────────────────────────────────────────────
   A module that is not playable yet should read as deliberately held back,
   not as a broken tile. The poster slice drains to grey, a fine diagonal
   hatch lays over it (the "not yet" signal that survives any client palette),
   the picto ghosts back, and the name steps down to muted. Closed goes one
   step further and stops taking taps. */
.tile.soon,.tile.closed{background:color-mix(in srgb,var(--secondary-color) 4%,transparent)}
.tile.soon .art,.tile.closed .art{filter:grayscale(1) contrast(.85) brightness(.62)}
.tile.soon .art::after,.tile.closed .art::after{
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--background-color) 94%, transparent) 0%,
    color-mix(in srgb,var(--background-color) 78%, transparent) 60%,
    color-mix(in srgb,var(--background-color) 62%, transparent) 100%)}
/* the hatch — 6px pitch, drawn from --ink so it works on a light theme too */
.tile.soon::before,.tile.closed::before{content:'';position:absolute;inset:0;z-index:1;
  pointer-events:none;
  background:repeating-linear-gradient(-45deg,
    transparent 0 5px,
    color-mix(in srgb,var(--secondary-color) 7%,transparent) 5px 6px)}
.tile.soon .card-picto,.tile.closed .card-picto{opacity:.3;filter:grayscale(1)}
.tile.soon .name{color:var(--muted)}
.tile.soon:hover{box-shadow:none}
.tile.soon:hover .name{transform:none}
.tile.closed{pointer-events:none}
.tile.closed .name{color:color-mix(in srgb,var(--secondary-color) 38%,transparent);text-decoration:line-through;
  text-decoration-thickness:1px;text-decoration-color:color-mix(in srgb,var(--secondary-color) 30%,transparent)}
/* the badge is the one thing that keeps full contrast — it says why */
.tile.soon .badge,.tile.closed .badge{position:relative;z-index:2}

/* ═══ LIFE ON THE TILES ═══
   Everything here loops slowly and out of phase (--i is the card index, set in
   renderGrid) so the grid breathes instead of flashing. A fan reads this page
   between two possessions: motion has to be felt, never watched. All of it is
   killed under prefers-reduced-motion at the end of this block. */
@keyframes tileDrift{0%,100%{transform:scale(1.06)}50%{transform:scale(1.14) translateY(-1.5%)}}
@keyframes accentShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
@keyframes tileSheen{0%{transform:translateX(-140%) skewX(-12deg)}
                     55%,100%{transform:translateX(280%) skewX(-12deg)}}
@keyframes livePulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--secondary-color) 55%,transparent)}
                     70%,100%{box-shadow:0 0 0 13px transparent}}

/* the flat accent gradient slowly travels instead of sitting still */
.tile .art{background-size:190% 190%;
  animation:accentShift 16s ease-in-out infinite;
  animation-delay:calc(var(--i,0) * -2.1s)}
/* photos drift — a slow push-in, never a pan that reveals a crop edge */
.tile.photo .art img{transform:scale(1.06);transform-origin:52% 46%;
  animation:tileDrift 26s ease-in-out infinite;
  animation-delay:calc(var(--i,0) * -3.4s);
  transition:filter .3s}
/* ── THE WALL MOVES ───────────────────────────────────────────────────
   MODULE_STYLES.photos ships several frames of the module running; the tile
   stacks them and dissolves between them, so the card previews the thing
   instead of picturing it. Layer 0 sits on top and each next one below it
   (negative z-index keeps them all under the .art scrim), and every layer
   shares ONE cycle with a phase shift of cycle/n — so layer k is opaque for
   its slice, then fades out onto layer k+1 already at full opacity. Nothing
   is ever mid-fade over the bare gradient, and the loop closes on itself
   because layer 0 fades back in before the last one fades out.
   Only feat/wide tiles get frames (renderGrid decides): a square tile is too
   small to read a dissolve and the extra downloads would not earn their 4G. */
@keyframes tileShuffle2{0%,50%{opacity:1}53.5%,96.5%{opacity:0}100%{opacity:1}}
@keyframes tileShuffle3{0%,33.333%{opacity:1}36.833%,96.5%{opacity:0}100%{opacity:1}}
@keyframes tileShuffle4{0%,25%{opacity:1}28.5%,96.5%{opacity:0}100%{opacity:1}}
.tile.photo .art--shuffle > *{
  z-index:calc(-1 - var(--k,0));
  animation-duration:26s,var(--shuffle,15s);
  animation-timing-function:ease-in-out,ease-in-out;
  animation-iteration-count:infinite,infinite;
  /* drift keeps its per-tile stagger; the dissolve takes its phase from --k */
  animation-delay:calc(var(--i,0) * -3.4s),
                  calc(-1 * var(--k,0) * var(--shuffle,15s) / var(--n,1))}
.tile.photo .art--n2 > *{animation-name:tileDrift,tileShuffle2}
.tile.photo .art--n3 > *{animation-name:tileDrift,tileShuffle3}
.tile.photo .art--n4 > *{animation-name:tileDrift,tileShuffle4}
/* hold each frame ~4s. Deliberately not round: 16s would lock step with the
   accent drift underneath and the two would pulse together. */
.art--n2{--shuffle:9.2s} .art--n3{--shuffle:12.9s} .art--n4{--shuffle:16.6s}
/* ── THE CLIENT'S OWN WALL ─────────────────────────────────────────────
   hydrateFanwallTiles() replaces the stock frames with the real posts from
   this game's own Fan Wall JSON, so the tile shows THIS client's fans — the
   same moderated photos already up on their screen — and no stock shot of
   somebody else's crowd. Six cells read as a wall at tile size; fewer posts
   fall back to full-bleed frames, none at all to the stock loop.
   Cells do not drift: six independent push-ins is a washing machine. */
.tile.photo .art .wall{position:absolute;inset:0;display:grid;
  /* minmax(0,1fr), never plain 1fr: a track's automatic minimum is its
     content's intrinsic size, so six 1024px photos push the grid out to
     3x their own size and the mosaic overflows the tile it sits in */
  grid-template-columns:repeat(3,minmax(0,1fr));
  grid-template-rows:repeat(2,minmax(0,1fr));gap:2px}
.tile.photo .art .wall img{position:relative;inset:auto;
  width:100%;height:100%;max-width:none;margin:0;min-width:0;min-height:0;
  object-fit:cover;object-position:50% 38%;
  animation:none;transform:none}
/* one tile is 130-190px tall: three columns is already small, and on a square
   tile the cells stop being faces. Two up, two across there. */
.tile.photo:not(.feat):not(.wide) .art .wall{grid-template-columns:repeat(2,minmax(0,1fr))}
/* A mosaic layer does NOT drift: the push-in is there so a single photo never
   shows its crop edge, but on a six-cell grid it just scales the whole wall up
   and eats the outer cells. Walls cross-fade and hold still — so they need the
   shuffle timing on their own, without tileDrift riding along in the list. */
.tile.photo .art--shuffle > .wall{transform:none;
  animation-duration:var(--shuffle,15s);
  animation-delay:calc(-1 * var(--k,0) * var(--shuffle,15s) / var(--n,1))}
.tile.photo .art--n2 > .wall{animation-name:tileShuffle2}
.tile.photo .art--n3 > .wall{animation-name:tileShuffle3}
.tile.photo .art--n4 > .wall{animation-name:tileShuffle4}
/* A client's own posts are whatever the fans sent — a white team crest is as
   likely as a dark selfie, so the scrim the stock frames were tuned against is
   not enough here. A live wall gets the name and notice back on a firm plate:
   the tile still has to be readable when the cell behind the text is white. */
.tile.photo:has(.wall) .art::after{
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--tile-background-color) 93%, transparent) 0%,
    color-mix(in srgb,var(--tile-background-color) 58%, transparent) 46%,
    color-mix(in srgb,var(--tile-background-color) 6%, transparent) 100%)}


/* a held-back module should not run a slideshow of what you cannot play yet */
.tile.soon .art--shuffle > *,.tile.closed .art--shuffle > *{animation-play-state:paused}

/* one light sweep across the card, staggered per tile */
.tile::after{content:"";position:absolute;z-index:1;pointer-events:none;
  top:-45%;bottom:-45%;left:0;width:26%;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.13),transparent);
  filter:blur(3px);
  animation:tileSheen 9s ease-in-out infinite;
  animation-delay:calc(var(--i,0) * 1.15s + 1.8s)}
/* a live module should feel like it is happening right now */
.tile[data-status="live"]{animation:livePulse 2.6s ease-out infinite}
/* touch/hover: the picto lifts, the art warms up */
.card-picto{transition:transform .22s cubic-bezier(.2,.7,.2,1)}
.tile:hover .card-picto{transform:translateY(-3px) scale(1.07)}
.tile:hover .art img{filter:saturate(1.18) contrast(1.04)}
.tile:active .card-picto{transform:translateY(-1px) scale(1.02)}

@media(prefers-reduced-motion:reduce){
  .tile .art,.tile.photo .art img,.tile.photo .art--shuffle > *,
  .tile::after,.tile[data-status="live"]{animation:none}
  .tile.photo .art img{transform:scale(1.04)}
}
/* featured tile: ticket-notch + PLAY pass */
.play{position:relative;z-index:3;display:inline-flex;align-items:center;gap:8px;
  font-family:var(--font-family);font-weight:900;font-size:14px;letter-spacing:.1em;text-transform:uppercase;
  padding:11px 16px 11px 18px;border-radius:12px;color:var(--background-color);background:var(--secondary-color);
  box-shadow:0 12px 26px -12px rgba(0,0,0,.7);margin-top:2px;
  -webkit-mask:radial-gradient(9px at 0 50%,transparent 98%,#000) left/51% 100% no-repeat,
               radial-gradient(9px at 100% 50%,transparent 98%,#000) right/51% 100% no-repeat;
          mask:radial-gradient(9px at 0 50%,transparent 98%,#000) left/51% 100% no-repeat,
               radial-gradient(9px at 100% 50%,transparent 98%,#000) right/51% 100% no-repeat}
.play svg{width:15px;height:15px;fill:var(--background-color)}

/* ═══════════════════ BOTTOM DOCK ═══════════════════
   Replaces the old auto-scrolling link slider. That marquee moved every link
   out from under the fan's thumb on a timer; here the links sit still behind
   one burger, and the motion is spent on information that actually changes —
   the game on court, the last results, what is next. */
/* ── shared dock identity ──────────────────────────────
   The bottom bar is the same component on all 109 pages, so it wears the same
   clothes here as it does on schedule.html and the rest: one dark grey sheet,
   white type, one condensed face. The tokens are copied from css/ext-slider.css
   (the source of truth) because the landing does not load that file — keep the
   two in sync. paintDockPalette() in landingb.js applies the client's own
   background_color / primary_color over them under the same contrast test
   extApplyPalette() uses, so a game that themes the bar on the module pages
   themes it identically here.
────────────────────────────────────────────────────── */
.dock,.dock-menu{
  /* Defaults only — paintDockPalette() overwrites all three from the
     background now showing. They are var()-backed so the pre-JS frame is not a
     dark slab on a light config. */
  --es-bg:    var(--background-color,#1c1d21);
  --es-panel: color-mix(in srgb,var(--background-color,#1c1d21) 86%,var(--ink,#fff));
  --es-ink:   var(--ink,#ffffff);
  --es-live:  #ff4757;
  --es-font: 'Oswald','Barlow Condensed','Archivo Narrow',system-ui,sans-serif;
  --es-w-reg: 300;
  --es-w-bold: 700;
}
.dock{font-family:var(--es-font);letter-spacing:.02em;
  position:fixed;left:50%;transform:translateX(-50%);bottom:0;z-index:150;
  width:min(440px,100%);display:flex;align-items:stretch;gap:0;
  background:var(--es-bg);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
  border-top:1px solid color-mix(in srgb,var(--secondary-color) 13%,transparent);
  padding-bottom:env(safe-area-inset-bottom,0px)}
.dock[hidden]{display:none}
.dock-burger{flex:0 0 auto;width:56px;display:grid;align-content:center;justify-items:center;gap:4px;
  background:transparent;border:none;border-right:1px solid color-mix(in srgb,var(--secondary-color) 13%,transparent);
  cursor:pointer;padding:0}
.dock-burger span{display:block;width:19px;height:2px;border-radius:2px;background:var(--es-ink);
  transition:transform .24s cubic-bezier(.2,.75,.2,1),opacity .16s ease}
.dock-burger[aria-expanded="true"] span:nth-child(1){transform:translateY(6px) rotate(45deg)}
.dock-burger[aria-expanded="true"] span:nth-child(2){opacity:0}
.dock-burger[aria-expanded="true"] span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}
.dock-burger:active{background:color-mix(in srgb,var(--secondary-color) 8%,transparent)}

/* the ticker itself — one lane, paused on touch so a fan can read it */
.dock-ticker{flex:1;min-width:0;overflow:hidden;height:52px;display:flex;align-items:center;
  -webkit-mask:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent)}
.dock-track{display:flex;align-items:center;gap:0;white-space:nowrap;width:max-content;
  animation:dockRoll linear infinite}
.dock-ticker:active .dock-track{animation-play-state:paused}
@keyframes dockRoll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.dock-item{display:inline-flex;align-items:center;gap:7px;padding:0 16px;
  font-size:13px;font-weight:var(--es-w-reg);color:var(--es-ink)}
.dock-item + .dock-item{border-left:1px solid color-mix(in srgb,var(--secondary-color) 13%,transparent)}
.dock-tag{font-weight:var(--es-w-bold);font-size:10px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--es-ink)}
.dock-item.is-live .dock-tag{color:var(--es-live)}
.dock-item.is-live .dock-tag::before{content:"";display:inline-block;width:6px;height:6px;
  border-radius:50%;background:currentColor;margin-right:6px;vertical-align:middle;
  animation:blink 1.1s ease-in-out infinite}
.dock-item img{width:20px;height:15px;object-fit:cover;border-radius:2px;flex-shrink:0}
.dock-item b{font-weight:var(--es-w-bold)}
.dock-item .dock-score{font-weight:var(--es-w-bold);font-variant-numeric:tabular-nums;color:var(--es-ink)}

/* the menu the burger opens */
.dock-menu{font-family:var(--es-font);
  position:fixed;inset:0;z-index:200;display:flex;align-items:flex-end;justify-content:center;
  background:rgba(0,0,0,.5);backdrop-filter:blur(3px);
  animation:dockFade .18s ease both}
.dock-menu[hidden]{display:none}
@keyframes dockFade{from{opacity:0}to{opacity:1}}
@keyframes dockRise{from{transform:translateY(14px);opacity:0}to{transform:none;opacity:1}}
@keyframes dockSink{from{transform:none;opacity:1}to{transform:translateY(18px);opacity:0}}
@keyframes dockRowIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
/* closing is its own state: the sheet slides back down instead of vanishing */
.dock-menu.is-closing{animation:dockFade .2s ease reverse both}
.dock-menu.is-closing .dock-menu-panel{animation:dockSink .2s cubic-bezier(.4,0,1,1) both}
/* rows arrive after the panel, in order — --i is set per row in renderDockMenu */
.dock-menu-list > *{animation:dockRowIn .3s cubic-bezier(.2,.75,.2,1) both;
  animation-delay:calc(90ms + var(--i,0) * 26ms)}
.dock-menu.is-closing .dock-menu-list > *{animation:none}
.dock-menu-panel{width:min(440px,100%);max-height:76vh;display:flex;flex-direction:column;
  background:var(--es-bg);
  border-top:1px solid color-mix(in srgb,var(--secondary-color) 16%,transparent);
  border-radius:18px 18px 0 0;padding-bottom:env(safe-area-inset-bottom,0px);
  animation:dockRise .26s cubic-bezier(.2,.75,.2,1) both}
.dock-menu-head{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;
  padding:16px var(--gutter) 10px;
  font-weight:var(--es-w-bold);font-size:13px;
  letter-spacing:.14em;text-transform:uppercase;color:color-mix(in srgb,var(--secondary-color) 60%,transparent)}
.dock-close{background:none;border:none;color:var(--es-ink);font-size:26px;line-height:1;
  cursor:pointer;padding:0 4px;min-width:44px;min-height:44px}
/* min-height:0 — see the note in ext-slider.css: without it the list refuses
   to shrink inside the flex column and overflows the 76vh panel. */
.dock-menu-list{overflow-y:auto;min-height:0;-webkit-overflow-scrolling:touch;
  padding:0 var(--gutter) 18px;
  display:grid;grid-template-columns:1fr 1fr;gap:9px}

/* Rows reuse .away-btn markup, but inside the sheet they answer to the dock's
   palette, not the page's — otherwise the client's ink lands on the dark sheet
   and half the menu goes unreadable. Scoped so the "Elsewhere" buttons in the
   page body keep the client's own look. */
.dock-menu-list .away-btn{font-family:var(--es-font);color:var(--es-ink);
  background:var(--es-panel);border:1px solid color-mix(in srgb,var(--secondary-color) 12%,transparent)}
.dock-menu-list .away-btn:hover{background:var(--es-panel);
  border-color:color-mix(in srgb,var(--secondary-color) 55%,transparent);color:var(--es-ink)}
.dock-menu-list .away-btn .away-name{font-family:var(--es-font);
  font-weight:var(--es-w-bold);font-size:13px;letter-spacing:.04em}
/* the name is client HTML — a config that puts a sponsor mark in it must not
   have the image stretched to whatever the row happens to be */
.dock-menu-list .away-btn .away-name img{height:auto;max-width:100px;}
.dock-menu-list .away-btn .away-ic{background:color-mix(in srgb,var(--secondary-color) 20%,transparent)}
.dock-menu-list .away-btn .away-ic span{font-family:var(--es-font);
  font-weight:var(--es-w-bold);font-size:15px;color:var(--es-ink)}
.dock-menu-list .away-btn .away-go{stroke:color-mix(in srgb,var(--secondary-color) 62%,transparent)}
.dock-menu-list .away-btn:hover .away-go{stroke:var(--es-ink);transform:translate(2px,-2px)}
/* a labelled rule between the modules and the outbound links */
.dock-sep{grid-column:1/-1;display:flex;align-items:center;gap:10px;margin:6px 0 1px;
  font-family:var(--es-font);font-weight:var(--es-w-bold);font-size:10px;
  letter-spacing:.18em;text-transform:uppercase;color:color-mix(in srgb,var(--secondary-color) 60%,transparent)}
.dock-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,var(--secondary-color) 14%,transparent)}
/* module pictos fill their slot rather than sitting on a tinted chip */
.away-ic--picto{background:transparent!important}
.away-ic--picto .card-picto{width:100%;min-width:0;height:100%;min-height:0;filter:none}
.dock-mod.is-dead{opacity:.5}
.dock-flag{flex:0 0 auto;font-family:var(--es-font);font-weight:var(--es-w-bold);
  font-size:10px;letter-spacing:.12em;text-transform:uppercase;
  color:color-mix(in srgb,var(--secondary-color) 60%,transparent);border:1px solid color-mix(in srgb,var(--secondary-color) 26%,transparent);
  border-radius:5px;padding:2px 6px}
@media(prefers-reduced-motion:reduce){
  .dock-track{animation:none}
  .dock-menu,.dock-menu-panel{animation:none}
  .dock-item.is-live .dock-tag::before{animation:none}
}

/* ═══════════════════ ELSEWHERE ═══════════════════
   Outbound links leave the fan zone, so they are not tiles — but they were a
   list of hairline rows, which read as an afterthought under a grid of cards.
   They are buttons now, sharing the section head the modules use. */
.away[hidden]{display:none}
/* a subtitle, not a second section head — the modules grid above owns the
   page's one big heading */
.sec--sub{padding:30px var(--gutter) 10px}
.sec--sub h2{font-size:calc(var(--fs) * 0.75);letter-spacing:.14em;color:var(--muted)}
.away-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px;padding:0 var(--gutter)}
.away-btn{position:relative;display:flex;align-items:center;gap:10px;min-height:56px;
  padding:11px 12px;border-radius:13px;text-decoration:none;
  background:color-mix(in srgb,var(--secondary-color) 6%,transparent);
  border:1px solid color-mix(in srgb,var(--secondary-color) 12%,transparent);
  color:var(--secondary-color);transition:background .18s,border-color .18s,transform .12s}
.away-btn:hover{background:color-mix(in srgb,var(--primary-color) 14%,transparent);
  border-color:color-mix(in srgb,var(--primary-color) 55%,transparent);color:var(--secondary-color)}
.away-btn:active{transform:scale(.985)}
/* a link with artwork in the JSON shows it; the rest get an accent monogram */
.away-btn .away-ic{width:34px;height:34px;flex:0 0 auto;border-radius:9px;overflow:hidden;
  display:grid;place-items:center;
  background:color-mix(in srgb,var(--primary-color) 20%,transparent)}
.away-btn .away-ic img{width:100%;height:100%;max-width:none;margin:0;object-fit:cover;display:block}
.away-btn .away-ic span{font-family:var(--font-family);font-weight:900;
  font-size:calc(var(--fs) * 0.6);line-height:1;color:var(--primary-color)}
.away-btn .away-name{flex:1;min-width:0;font-family:var(--font-family);font-weight:800;
  font-size:calc(var(--fs) * 0.72);line-height:1.05;letter-spacing:.02em;text-transform:uppercase;
  /* break at spaces; only split a word that cannot fit a line on its own */
  overflow-wrap:break-word;word-break:normal;hyphens:none}
.away-btn .away-go{width:14px;height:14px;flex:0 0 auto;fill:none;stroke:var(--muted);
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:stroke .18s,transform .18s}
.away-btn:hover .away-go{stroke:var(--primary-color);transform:translate(2px,-2px)}
/* an odd count leaves a gap — the last button spans it rather than half-filling */
/* Scoped to the flat "More info" grid on purpose. In the dock menu the rows
   are modules + a separator + links, so nth-child parity there is meaningless
   — 4 modules + sep + 2 links makes the last link nth-child(7) and it used to
   stretch across both columns, stranding its partner. The dock marks its own
   wide rows with .is-wide, per group. */
.away-grid .away-btn:last-child:nth-child(odd){grid-column:span 2}
.dock-menu-list .away-btn.is-wide{grid-column:span 2}

/* ── JUMBOTRON: NO PHONE FRAME ──────────────────────────────────────────
   .frame is a 440px phone column with rounded corners and a backdrop — right
   for a fan holding a handset, wrong for a stadium screen. state=scan is
   always a big display, so the frame is dissolved and the overlay owns the
   viewport. body.scan is set by the scan branch in landingb.js. */
body.scan{padding:0!important;display:block!important;background:var(--background-color)}
body.scan .frame{
  width:100%!important;max-width:none!important;min-height:100vh!important;
  border-radius:0!important;box-shadow:none!important;padding-bottom:0!important;
  opacity:1!important;
  /* background-image deliberately NOT reset: applyConfig paints the desktop
     key visual here for the jumbotron, and !important:none would erase it. */
}
body.scan .field,body.scan .dock,body.scan .dock-menu,body.scan .now-slot{display:none!important}

/* ═══════════════════ SCAN / QR (jumbotron) ═══════════════════
   Ported from css/landingd.css so ?state=scan behaves as on landing.html. */
.scan-overlay{display:flex;flex-direction:row;height:100vh;position:fixed;inset:0;z-index:50}
.scan-overlay[hidden]{display:none!important}
.scan-qr{width:50%;display:flex;align-items:center;justify-content:center;padding:20px}
.scan-qr img{width:calc(var(--qr-size,12vw) * 4);min-width:350px;max-width:100%;height:auto;border-radius:12px}
.scan-text{width:50%; corner-shape:squircle;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px}
/* the takeover logo is sized by the back office too — same intro.header_logo_size
   the bar uses, applied as a WIDTH here because the QR panel is a column and the
   logo is what sets its visual weight. 280px stays the fallback for a config that
   never set one; max-width keeps it inside a narrow frame either way. The
   height cap is viewport-relative, not a flat 200px: scan draws on a stadium
   screen, so a tall logo has to scale with the display rather than stay the
   same pixel height on a 4K jumbotron as on a laptop. */
.scan-logo{display:block;width:var(--header-logo-size,280px);max-width:100%;max-height:40vh;height:auto;object-fit:contain;margin:0 auto 2rem}
.scan-title{font-family:var(--font-family);font-size:calc(var(--font-size)*1.5);line-height:1;font-weight:900;text-transform:uppercase;color:var(--primary-color);font-style:italic;text-shadow:3px 1px 2px var(--secondary-color);text-align:center}
.scan-intro{margin-top:1rem;font-size:calc(var(--font-size, 21px)*2);color:var(--primary-color);line-height:1;text-align:center}
/* QR position variants (driven by intro.qr_position from JSON) */
.scan-overlay[data-qr-pos="right"]{flex-direction:row-reverse}
.scan-overlay[data-qr-pos="center"]{flex-direction:column;align-items:center;justify-content:center;gap:1rem}
.scan-overlay[data-qr-pos="center"] .scan-qr{width:auto;padding:20px 20px 0}
.scan-overlay[data-qr-pos="center"] .scan-text{width:auto;max-width:90%;padding:0 40px 40px}
@media(max-width:768px){
  .scan-overlay{display:none!important}
}

/* ═══════════════════ PREVIEW: QR + LIVE PHONE ═══════════════════
   ?state=preview keeps the QR panel exactly as scan draws it and gives the
   other half of the screen to a phone playing this same landing. Two columns,
   never three: .scan-left holds the QR with its logo/title/intro so the
   overlay only ever has to balance [copy] against [phone]. */
.scan-overlay[data-mode="preview"]{
  display:flex;flex-direction:row;align-items:center;justify-content:center;
  gap:clamp(24px,5vw,96px);padding:clamp(20px,4vh,56px) clamp(24px,4vw,72px);
}
/* qr_position=right puts the phone first and the copy second */
.scan-overlay[data-mode="preview"][data-qr-pos="right"]{flex-direction:row-reverse}
/* the copy takes what the phone leaves, capped so the QR and the title stay a
   block the eye reads as one panel instead of drifting apart on a 4K screen */
.scan-left{display:flex;flex-direction:column;align-items:center;justify-content:center;
  flex:0 1 auto;min-width:0;max-width:min(52vw,900px);gap:2vh}
/* inside the column both panels are content-sized, not half-screen */
.scan-overlay[data-mode="preview"] .scan-qr,
.scan-overlay[data-mode="preview"] .scan-text{width:100%;padding:0}
/* the QR shares the screen with the phone now, so it is sized off the viewport
   height rather than the old min-width:350px, which overflowed a 1280px screen */
.scan-overlay[data-mode="preview"] .scan-qr img{
  width:min(var(--qr-size,12vw) * 4, 70vh);min-width:240px}
/* logo and copy share one row under the QR: stacked, the logo alone ate ~20vh
   of a column that has to fit beside the phone, and that height is what the QR
   gets back above. Logo on the left, title and intro stacked on its right. */
.scan-logo[hidden]{display:none}
.scan-overlay[data-mode="preview"] .scan-text{
  display:grid;grid-template-columns:max-content minmax(0,auto);column-gap:clamp(16px,2vw,40px);
  align-items:center;justify-content:center}
.scan-overlay[data-mode="preview"] .scan-logo{
  grid-row:1 / span 2;width:auto;max-width:min(var(--header-logo-size,280px),14vw);
  max-height:16vh;margin:0}
.scan-overlay[data-mode="preview"] .scan-title,
.scan-overlay[data-mode="preview"] .scan-intro{grid-column:-2;text-align:start}
.scan-overlay[data-mode="preview"] .scan-intro:empty{display:none}
/* the configured sizes are set for a full-height column; in a row they are
   capped to the screen height so a long title cannot push the QR back down */
.scan-overlay[data-mode="preview"] .scan-title{font-size:min(calc(var(--font-size)*1.5),6.5vh)}
.scan-overlay[data-mode="preview"] .scan-intro{text-transform:uppercase;font-size:min(calc(var(--font-size, 21px)*2),4.5vh);margin-top:1vh}
/* no logo: the copy is the whole row, centred as before */
.scan-overlay[data-mode="preview"] .scan-text:has(> .scan-logo[hidden]){grid-template-columns:minmax(0,auto)}
.scan-overlay[data-mode="preview"] .scan-text:has(> .scan-logo[hidden]) :is(.scan-title,.scan-intro){text-align:center}
.scan-overlay[data-mode="preview"] #scanTitle{display:none}
/* THE PHONE — a handset-shaped viewport, not a picture of one: the iframe is
   the live fan page. Height drives the size so the device always fits the
   screen; 9/19.5 is the modern handset aspect. */
.scan-phone{display:flex;align-items:center;justify-content:center;height:100%}
.scan-phone[hidden]{display:none!important}
.scan-phone__device{
  position:relative;height:min(92vh,1000px);aspect-ratio:9/19.5;max-width:38vw;
  border-radius:clamp(28px,3vh,46px);overflow:hidden;
  background:var(--background-color);
  border:clamp(6px,.8vh,12px) solid rgba(0,0,0,.82);
  box-shadow:0 2vh 6vh rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.12) inset;
}
.scan-phone__view{display:block;width:100%;height:100%;border:0;background:transparent}

/* the preview is a display, not a control: nothing on the takeover should look
   tappable, and a stray touch must not scroll the phone out of its loop */
.scan-phone__device{pointer-events:none}

/* ═══════════════════ EMBEDDED FAN PAGE (?embed=1) ═══════════════════
   The page inside .scan-phone__device. It stays the real mobile landing —
   only the scrollbar goes, because the phone has no scrollbar. */
/* smooth scrolling is for a thumb; this page is scrolled frame by frame */
html.embed{scroll-behavior:auto}
html.embed,body.embed{overflow-x:hidden;scrollbar-width:none;-ms-overflow-style:none}
html.embed::-webkit-scrollbar,body.embed::-webkit-scrollbar{display:none}
/* the desktop backdrop and the 520px rounded-frame treatment are for a browser
   window; inside the device the landing owns every pixel */
body.embed .frame{width:100%!important;max-width:none!important;
  border-radius:0!important;box-shadow:none!important;margin:0!important}


/* ═══════════════════════════════════════
    MODAL
═══════════════════════════════════════ */

/* ═══════════════════ PARTNER BANNER ═══════════════════ */
/* ═══ BANNER SLIDER ═══
   is_banner playlist items are artwork the client paid to place, so they run
   full bleed — no gutter, no radius, no border boxing them in. Scroll-snap
   handles the swipe natively; renderBanner() only auto-advances and keeps the
   dots in step. */
.partner[hidden]{display:none}
/* same box as the sports panel above: the slide width is the panel width, so
   a banner lines up with the widget instead of bleeding past it */
.partner{margin:22px var(--gutter) 0;position:relative;background:var(--surface);
  border-radius:var(--panel-r);overflow:hidden}
.partner-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;
  scrollbar-width:none;-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain}
.partner-track::-webkit-scrollbar{display:none}
.partner-slide{flex:0 0 100%;scroll-snap-align:center;scroll-snap-stop:always;
  display:block;text-decoration:none}
.partner-slide img{width: auto;
    height: auto;
    display: block;
    max-height: 25vh;
    text-align: center;
    margin: 0 auto;
    max-width: 100%;}
.pdots{position:absolute;bottom:9px;left:0;right:0;display:flex;gap:5px;
  justify-content:center;z-index:2;pointer-events:none}
.pdots:empty{display:none}
.pdots i{width:6px;height:6px;border-radius:50%;background:color-mix(in srgb,var(--secondary-color) 45%,transparent);
  box-shadow:0 1px 3px rgba(0,0,0,.5);transition:width .2s ease,background .2s ease}
.pdots i.on{background:var(--secondary-color);width:16px;border-radius:3px}

/* ═══════════════════ SPONSOR SLIDER ═══════════════════ */
.slider{overflow:hidden;padding:0 0 6px;-webkit-mask:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent)}
.slide-track{display:flex;gap:10px;padding:0 var(--gutter);animation:marq2 26s linear infinite;width:max-content}
/* Official Partners are logos and nothing else — no name, no strapline.
   Source is intro.partner_logo / the is_slider playlist items' `image`. */
.scard{flex:0 0 auto;display:grid;place-items:center;padding:12px 18px;border-radius:14px;
  background:var(--surface);border:1px solid var(--hair);height:66px;min-width:120px}
.scard img{max-height:42px;width:auto;max-width:150px;object-fit:contain}

/* ═══════════════════ MOTION ═══════════════════ */
@keyframes blink{0%,100%{opacity:1}50%{opacity:.28}}
@keyframes marq2{from{transform:translateX(0)}to{transform:translateX(-50%)}}
:focus-visible{outline:2px solid var(--primary-color);outline-offset:3px;border-radius:6px}
@media(prefers-reduced-motion:reduce){
  *{animation-duration:.001ms!important;animation-iteration-count:1!important}
.tile{opacity:1;transform:none}
}
      /* Wiclax marathon results: category filter + runner rows. Kept here with
         the widget JS that emits the markup rather than in landingd.css. */
      .sw-filter{display:flex;gap:4px;overflow-x:auto;padding:4px 2px 6px;scrollbar-width:none;-webkit-overflow-scrolling:touch}
      .sw-filter::-webkit-scrollbar{display:none}
      .sw-chip{flex:0 0 auto;padding:3px 9px;border:1px solid color-mix(in srgb,var(--secondary-color) 14%,transparent);border-radius:999px;
        corner-shape:squircle;background:transparent;color:color-mix(in srgb,var(--secondary-color) 55%,transparent);font:inherit;font-weight:700;
        letter-spacing:.04em;text-transform:uppercase;cursor:pointer;transition:all .15s}
      .sw-chip:hover{color:var(--secondary-color);border-color:color-mix(in srgb,var(--secondary-color) 30%,transparent)}
      /* Filled with the theme accent only as a tint: a solid fill would put
         theme text on theme background (both white on this game) and vanish. */
      .sw-chip.active{background:color-mix(in srgb,var(--secondary-color) 16%,transparent);border-color:var(--primary-color);color:var(--primary-color)}
      /* .sw-standings left-aligns column 2 (the team cell). Here column 2 is
         the bib and column 3 is the runner, so the alignment shifts one over. */
      /* The shared widget palette is white-on-glass, which disappears on a pale
         theme (Seine Marathon: #c2e4e7 background). The results block carries
         its own dark surface so the rows stay readable whatever the game JSON
         sets, without touching the shared .sports-widget rules. */
      /* A refresh keeps the widget's footprint: the block holds its height,
         the old rows stay in place dimmed under a small spinner, and the new
         ones take over in one frame. Without this the 30s auto-refresh emptied
         the block and the whole page jumped. */
      .sw-content{position:relative;transition:min-height .3s ease}
      .sw-content.sw-refreshing > *{opacity:.35;transition:opacity .18s ease;pointer-events:none}
      .sw-content.sw-refreshing::after{content:'';position:absolute;top:10px;right:10px;width:14px;height:14px;
        border:2px solid color-mix(in srgb,var(--secondary-color) 18%,transparent);border-top-color:var(--primary-color);border-radius:50%;
        animation:sw-spin .7s linear infinite;z-index:6}
      @keyframes sw-spin{to{transform:rotate(360deg)}}
      .sw-wiclax{position:relative;background:rgba(6,16,24,.74);border-radius:10px; corner-shape:squircle;padding:4px}
      .sw-search{position:relative;padding:2px 2px 0}
      .sw-search-input{width:100%;box-sizing:border-box;padding:7px 26px 7px 10px;border-radius:10px;
        corner-shape:squircle;border:1px solid color-mix(in srgb,var(--secondary-color) 14%,transparent);background:color-mix(in srgb,var(--secondary-color) 8%,transparent);
        color:var(--secondary-color);font:inherit;text-transform:none;letter-spacing:0;outline:none}
      .sw-search-input::placeholder{color:color-mix(in srgb,var(--secondary-color) 35%,transparent)}
      .sw-search-input:focus{border-color:var(--primary-color);background:color-mix(in srgb,var(--secondary-color) 12%,transparent)}
      .sw-search-input::-webkit-search-cancel-button{display:none}
      .sw-search-clear{position:absolute;top:50%;right:9px;transform:translateY(-50%);border:none;background:none;
        color:color-mix(in srgb,var(--secondary-color) 45%,transparent);font-size:1.05rem;line-height:1;cursor:pointer;padding:0 2px}
      .sw-search-clear:hover{color:var(--secondary-color)}
      /* Overlays the rows instead of pushing them down, so the table does not
         jump every time a suggestion appears. */
      .sw-suggest{position:absolute;top:calc(100% + 2px);left:2px;right:2px;z-index:20;max-height:190px;overflow-y:auto;
        border-radius:10px; corner-shape:squircle;border:1px solid color-mix(in srgb,var(--secondary-color) 14%,transparent);
        background:rgba(12,20,28,.96);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
        box-shadow:0 10px 24px rgba(0,0,0,.35);padding:3px}
      .sw-suggest-item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 8px;border:none;background:none;
        border-radius:7px; corner-shape:squircle;color:inherit;font:inherit;text-align:left;cursor:pointer}
      .sw-suggest-item:hover{background:color-mix(in srgb,var(--secondary-color) 10%,transparent)}
      .sw-suggest-item .bib{min-width:30px;font-weight:700;color:color-mix(in srgb,var(--secondary-color) 45%,transparent);font-variant-numeric:tabular-nums}
      .sw-suggest-item .who{flex:1;font-size:.78rem;color:color-mix(in srgb,var(--secondary-color) 90%,transparent);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
      .sw-suggest-item .cat{font-size:.65rem;font-weight:700;color:var(--primary-color)}
      .sw-runners td{vertical-align:middle}
      .sw-runners th:nth-child(2),.sw-runners td:nth-child(2){text-align:center}
      .sw-runners th:nth-child(3),.sw-runners td:nth-child(3){text-align:left}
            /* Race pills: one per race in the feed, above the category chips. Same
         scrolling row as the chips, visually heavier because it filters a
         wider slice of the data. */
      .sw-races{padding-top:2px}
      .sw-pill{flex:0 0 auto;display:inline-flex;align-items:center;gap:5px;padding:4px 10px;
        border:1px solid color-mix(in srgb,var(--secondary-color) 16%,transparent);border-radius:999px; corner-shape:squircle;
        background:color-mix(in srgb,var(--secondary-color) 5%,transparent);color:color-mix(in srgb,var(--secondary-color) 62%,transparent);font:inherit;font-weight:700;
        font-size:.66rem;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;
        white-space:nowrap;transition:all .15s}
      .sw-pill:hover{color:var(--secondary-color);border-color:color-mix(in srgb,var(--secondary-color) 32%,transparent)}
      .sw-pill.active{background:color-mix(in srgb,var(--secondary-color) 16%,transparent);border-color:var(--secondary-color);color:var(--primary-color)}
      .sw-pill .n{font-size:.58rem;font-weight:800;opacity:.55}
      /* Rows are tappable: the detail panel underneath is the only place the
         last timing point is shown. */
      .sw-runners .sw-row{cursor:pointer}
      .sw-runners .sw-row:hover td{background:color-mix(in srgb,var(--secondary-color) 5%,transparent)}
      .sw-runners .sw-row.open td{background:color-mix(in srgb,var(--secondary-color) 8%,transparent)}
      .sw-runners .sw-row:focus-visible{outline:2px solid var(--secondary-color);outline-offset:-2px}
      /* .sw-standings centres its cells; the detail panel is prose, not data. */
      .sw-runners .sw-detail-row td{padding:0;background:color-mix(in srgb,var(--secondary-color) 7%,transparent);text-align:left}
      .sw-detail{display:flex;flex-direction:column;gap:3px;padding:7px 9px;text-transform:none}
      .sw-d-line{display:flex;align-items:baseline;gap:8px;line-height:1.25}
      .sw-d-line .k{flex:0 0 auto;min-width:88px;font-weight:700;letter-spacing:.06em;
        text-transform:uppercase;color:color-mix(in srgb,var(--secondary-color) 38%,transparent)}
      .sw-d-line .v{flex:1;min-width:0;color:color-mix(in srgb,var(--secondary-color) 82%,transparent);overflow:hidden;text-overflow:ellipsis}
      .sw-d-line .v b{font-weight:700;color:var(--secondary-color)}
      .sw-d-line .t{margin-left:6px;font-weight:800;color:var(--primary-color);font-variant-numeric:tabular-nums}
      .sw-d-line .gap{color:color-mix(in srgb,var(--secondary-color) 45%,transparent);font-weight:600}
      .sw-final{margin:4px 2px 0;padding:4px 6px;border-radius:8px;text-align:center;font-size:.62rem;
        font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--primary-color);
        background:color-mix(in srgb,var(--secondary-color) 8%,transparent);border:1px solid color-mix(in srgb,var(--secondary-color) 14%,transparent)}
.sw-runners .sw-bib{font-weight:700;color:color-mix(in srgb,var(--secondary-color) 45%,transparent);font-variant-numeric:tabular-nums}
      .sw-runners .sw-runner{display:flex;flex-direction:column;line-height:1.15;min-width:0}
      .sw-runners .sw-runner .name{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:color-mix(in srgb,var(--secondary-color) 92%,transparent)}
      .sw-runners .sw-runner .club{font-weight:500;text-transform:none;color:color-mix(in srgb,var(--secondary-color) 35%,transparent);
        white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
      .sw-runners .sw-cat{font-weight:700;color:color-mix(in srgb,var(--secondary-color) 40%,transparent)}
      .sw-runners .sw-time{text-align:right}
      .sw-runners .sw-time .sw-status{display:block;margin-bottom:1px}
      .sw-runners .sw-time .sw-net{font-weight:700;color:var(--primary-color);font-variant-numeric:tabular-nums;white-space:nowrap}

/* ── THIS GAME'S OWN FAN WALL, FULL-BLEED ──────────────────────────────
   hydrateFanwallTiles() adds .wall-live once the game's own moderated posts
   are up, and every layer is now ONE post covering the whole tile — no mosaic,
   no cells, no gutters. So the tile needs what the stock frames used to get:
   the picto steps back to a corner mark and lets the photograph carry the
   tile, and the scrim becomes a firm plate under the name, because a fan's
   post can be a white crest as easily as a dark selfie.
   Held-back tiles keep their own grey wash, hence the :not() guard. */
.tile.wall-live .art img{object-position:50% 40%}
.tile.wall-live .ic{align-self:flex-start;padding-top:10px;opacity:.7}
.tile.wall-live .card-picto{filter:drop-shadow(0 3px 10px rgba(0,0,0,.7))}
.tile.wall-live:not(.feat) .card-picto{width:2.6rem;min-width:2.6rem;height:2.6rem;min-height:2.6rem}
.tile.wall-live.feat .card-picto{width:4rem;min-width:4rem;height:4rem;min-height:4rem}
.tile.wall-live:not(.soon):not(.closed) .art::after{
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--tile-background-color) 93%, transparent) 0%,
    color-mix(in srgb,var(--tile-background-color) 58%, transparent) 46%,
    color-mix(in srgb,var(--tile-background-color) 6%, transparent) 100%)}
