/* ============================================================================
   RUCK DESIGN SYSTEM — v2
   ----------------------------------------------------------------------------
   A rebuild of the platform's visual language.

   Direction: the category convention, executed at full craft — held to the bar
   of Strava (figures that feel earned, warmth, celebration) and TeamSnap /
   Spond / Hudl (what club committees and coaches already expect). Professional
   first; the personality is spent on numbers, motion and moments, never on
   novelty. See PRODUCT.md -> Brand Commitments.

   THREE RULES THIS FILE MUST NOT BREAK
   ------------------------------------
   1. THE CLASS API IS A CONTRACT. Ten page modules and two components render
      against these names. This file changes how they LOOK, never what they are
      called, and never the markup they expect:
        - .sheet is a CHILD of .overlay; both are driven by `.open` (sheet.js)
        - .account-menu / .nav-drawer / .nav-scrim also toggle with `.open`
        - .toast toggles with `.show` (store.js)
        - the drawer brand uses .mark/.name; the topbar uses .brand-mark/.brand-name

   2. --brand-red / --brand-navy ARE PER-TENANT. applyBrandColors() in app.js
      overwrites them at runtime with each club's own colours. The names stay
      (historical, now tenant-agnostic) and --accent aliases them for new code.
      NOTHING here may assume the accent is red: every accent surface must
      survive any hue a club picks. That is precisely why this system is
      neutral-grounded with a single accent rather than a painted palette.

   3. Figures are tabular everywhere. Scores, money and stats must not jitter
      as values change during a live match.

   Ground: light by default. The deciding scene is a coach reading a phone in
   daylight at the side of a pitch, where dark surfaces wash out. Dark mode is
   designed for clubhouse and evening use, not merely inverted.

   Type: Archivo, self-hosted, one variable file (400-800). It was already
   loaded for scorelines and reached 4% of rendered lettering while the system
   stack carried the other 95% — the cost was paid and the voice was not. The
   variable cut is 34 KB against 68 KB for the two static weights it replaces,
   so the whole interface now speaks in one face for half the bytes, from this
   origin, offline.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   1. TOKENS
   ------------------------------------------------------------------------ */
:root {
  /* Brand — OVERWRITTEN AT RUNTIME per club. Defaults are the first club's. */
  --brand-red: #e84545;
  --brand-red-dark: #c93636;
  --brand-red-light: #ff6b6b;
  --brand-navy: #32353a;
  --brand-navy-dark: #212226;

  /* Semantic aliases — prefer these in new code. color-mix keeps the tints
     correct for ANY club colour, which a hard-coded rgba() could never do. */
  --accent: var(--brand-red);
  --accent-dark: var(--brand-red-dark);
  --accent-wash: color-mix(in srgb, var(--brand-red) 10%, transparent);
  --accent-wash-strong: color-mix(in srgb, var(--brand-red) 20%, transparent);
  --accent-ink: color-mix(in srgb, var(--brand-red) 72%, #14161c 28%);
  /* The club's colour AS A GROUND under lettering, and the lettering that goes
     on it. Both are recomputed per club by applyBrandColors() in app.js, which
     is the only place that knows the club's actual hue: a navy club keeps its
     colour and white type, a mid red is deepened a few percent so white clears
     AA, and a yellow club gets dark ink instead. No single constant can be
     right for all three, which is why this is solved in JS and not here.
     The values below are the answer for the default red, so the first paint
     before JS runs is already correct. */
  --accent-btn: color-mix(in srgb, var(--brand-red) 90%, var(--ink-shade));
  --on-accent: #ffffff;
  /* Chips sitting ON that ground — the hamburger, the bell, the brand mark,
     the avatar, the secondary top-bar buttons. A translucent WHITE veil (what
     these used before) lightens the ground and drags white lettering under AA
     on every mid-tone club colour. The veil therefore always moves AWAY from
     the lettering: darker under white ink, lighter under dark ink. */
  --accent-veil: color-mix(in srgb, var(--accent-btn) 86%, var(--ink-shade));
  --accent-veil-strong: color-mix(in srgb, var(--accent-btn) 74%, var(--ink-shade));
  /* The far end of the live bar's gradient. Same rule as the veil, further: it
     moves AWAY from --on-accent, so the gradient is never less readable than
     the ground it starts from. Also solved per club in brand.js. */
  --accent-deep: color-mix(in srgb, var(--accent-btn) 54%, var(--ink-shade));
  /* The accent AS TEXT. --accent itself is tuned to be a ground and is too
     light to read on one in dark mode; --accent-ink is a dark shade meant to
     BE a ground, and measures 2.6:1 as a foreground. Pulling the accent
     towards whatever --text-primary currently is keeps the club's colour
     legible in both themes, for any club colour. */
  --accent-text-light: color-mix(in srgb, var(--accent) 70%, #12161f);
  --accent-text-dark: color-mix(in srgb, var(--accent) 70%, #eef1f6);
  --accent-text: var(--accent-text-light);

  /* Neutral ramp — cool slate, even perceptual steps */
  --n-0: #ffffff;
  --n-25: #fafbfc;
  --n-50: #f4f6f8;
  --n-100: #e9edf1;
  --n-200: #dbe1e8;
  --n-300: #c2cad5;
  --n-400: #97a1b0;
  --n-500: #6a7484;
  --n-600: #4d5666;
  --n-700: #353d4a;
  --n-800: #212734;
  --n-900: #12161f;

  /* Semantic.
     ------------------------------------------------------------------------
     TWO TOKENS PER ROLE, and the difference matters.

       --x       is a GROUND. Solid fills, bars, dots, and buttons that carry
                 white lettering. It is chosen so white reads on it.
       --x-text  is a FOREGROUND. Badge lettering, figures, icons sitting on
                 --x-bg or on a card. It is chosen so it reads on those.

     One value cannot be both in dark mode: a colour light enough to read on a
     near-black card is too light to carry white text. Every one of these pairs
     used to be a single token, and every one of them measured below 4.5:1
     somewhere — .badge-success at 3.0:1, .badge-warning at 2.7:1, white on
     .btn-warning at 2.9:1. In light mode the two happen to coincide, so the
     alias is exact; dark mode overrides only the -text half. */
  --success: #137d55;
  --success-text: var(--success);
  --success-bg: #e6f7f0;
  --warning: #9d6005;
  --warning-text: var(--warning);
  --warning-bg: #fdf3e2;
  --danger: #c63348;
  --danger-text: var(--danger);
  --danger-bg: #fdecef;
  --info: #2c66d9;
  --info-text: var(--info);
  --info-bg: #eaf1fe;

  /* Surfaces */
  --bg-app: #f4f6f8;
  --bg-card: #ffffff;
  --bg-elevated: #ffffff;
  --bg-sunken: #eef1f5;
  --text-primary: #12161f;
  --text-secondary: #5b6472;
  /* Solved, not eyeballed: the old #8a93a3 measured 3.10:1 on a card, which
     put every .text-micro label below AA. This clears 4.5:1 on the card, the
     app ground and the sunken ground. The step below --text-secondary is now
     carried by size and weight rather than by making the text hard to read. */
  --text-tertiary: #666d7a;
  --border-subtle: #e8ebef;
  --border-default: #d8dde4;
  --border-strong: #c2cad5;

  /* Radius — moderated from v1: friendly, not bubbly */
  --r-xs: 6px;
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 18px;
  --r-xl: 24px;
  --r-full: 999px;

  /* Elevation — light. Hairline borders do the structural work, which reads
     cleaner in daylight and far better on inexpensive phone screens. */
  --shadow-xs: 0 1px 2px rgba(18, 22, 31, 0.05);
  --shadow-sm: 0 1px 3px rgba(18, 22, 31, 0.07), 0 1px 2px rgba(18, 22, 31, 0.04);
  --shadow-md: 0 4px 14px rgba(18, 22, 31, 0.09);
  --shadow-lg: 0 14px 36px rgba(18, 22, 31, 0.16);
  --shadow-red: 0 5px 16px color-mix(in srgb, var(--brand-red) 32%, transparent);
  --shadow-accent: var(--shadow-red);

  /* Motion */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  /* Smooth deceleration (ease-out-quint) rather than an overshoot. Real objects
     settle, they don't bounce back; the spring curve v1 used reads dated at the
     craft level this product is aiming for. --ease-spring is kept as an alias
     so existing references (pitch.css) keep working. */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: var(--ease-out);
  --t-fast: 0.13s;
  --t-med: 0.22s;
  --t-slow: 0.38s;

  /* Layout */
  --sidebar-w: 264px;
  --topbar-h: 64px;
  --bottomnav-h: 64px;
  --content-max: 1200px;

  --ink-shade: #0b0d12;       /* fixed dark used to shade a colour on hover; constant across themes so a hover never lightens */
  --captain-gold: #8b5e0a;    /* the captain's armband — a real rugby signal, not a UI accent */
  --captain-gold-text: var(--captain-gold);
  --captain-gold-bg: rgba(232, 161, 30, 0.15);
  /* --- EVENT HUES -----------------------------------------------------------
     The calendar needs CATEGORICAL colour: eight event types that must be told
     apart at four pixels across. The semantic palette carries meaning (good,
     bad, warning) and only has four, so these are their own scale — but they
     are TOKENS, defined once and redefined for dark, not eight hexes buried in
     a page module. A match takes the club's own accent, because a match is the
     club's event; the rest are fixed so a red club and a blue club still have
     eight distinguishable dots. */
  --event-training: #3d8bfd;
  --event-match: var(--accent);
  --event-tournament: #d98200;
  --event-meeting: #6b7280;
  --event-social: #12a06d;
  --event-fundraiser: #9333ea;
  --event-tour: #0d9488;
  --event-camp: #c2410c;
  --event-other: #7a8394;

  --notify: #ffd23f;          /* notification dot — deliberately outside the club accent so it reads on any brand colour */
  --pitch-turf: #137d55;      /* the grass on the squad builder — a pitch is green for every club, so this is NOT tenant-accented */
  --toast-ok: #4ade80;        /* toast success icon on the dark toast ground */
  --font-ui: "Archivo", "Archivo Fallback", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* The loud voice. It is the SAME family — a second face would have to do a
     job Archivo cannot, and at 400-800 it does not. The token survives because
     it marks which elements are allowed to shout (.text-hero, .text-mono-lg,
     .match-team, .match-score, .match-kickoff, .figure-value), and because it
     is the one seam a second face could ever land on. What separates the two
     voices now is weight and tracking, not family. */
  --font-display: var(--font-ui);

  /* --- The role scale ---------------------------------------------------
     Nine steps, named for the job. The old ramp put 10/11/12/13/14/15/16/17
     inside seven pixels and 86% of all rendered text landed in that band, so
     nothing could look more important than anything else. The bottom is now
     four steps, never closer than 2px, and every step also changes weight or
     colour — size is not asked to carry the hierarchy alone. */
  --fs-hero: clamp(30px, 7vw, 46px);
  --fs-score: 34px;   /* live scoreline: the largest figure in the product */
  --fs-match: 30px;   /* a played or upcoming fixture */
  --fs-page: 26px;    /* the page's own name */
  --fs-figure: 26px;  /* a KPI value */
  --fs-stat: 21px;    /* a secondary figure */
  --fs-title: 17px;   /* card and row titles */
  --fs-body: 15px;    /* reading copy, controls, table cells */
  --fs-label: 13px;   /* metadata, captions, hints */
  --fs-micro: 11px;   /* uppercase eyebrows and column heads */

  /* Weights. A variable font makes these continuous, so the intermediate
     steps below are real rather than rounded up to the nearest static cut. */
  --w-body: 400;
  --w-medium: 500;
  --w-strong: 620;
  --w-title: 700;
  --w-display: 800;

  /* Leading, tuned to Archivo rather than to one universal ratio: it is a
     squarish grotesque with a tall x-height, which reads tighter than its
     numbers suggest at display sizes and needs the air back in prose. */
  --lh-hero: 1.02;
  --lh-tight: 1.15;
  --lh-snug: 1.35;
  --lh-text: 1.55;

  --track-hero: -0.035em;
  --track-tight: -0.02em;
  --track-snug: -0.01em;
  --track-wide: 0.06em;

  /* Prose measure. Settings explainers were running to 171 characters on a
     desktop card — past the point where the eye finds the next line. */
  --measure: 72ch;

  color-scheme: light;
}

/* Archivo, self-hosted. One variable file per subset; latin-ext is fetched
   only if a name actually needs it, which Portuguese never does. */
@font-face {
  font-family: "Archivo";
  src: url("/static/fonts/archivo-latin.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Archivo";
  src: url("/static/fonts/archivo-latin-ext.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* The swap face. Measured against the real files in Chromium: Archivo runs
   878/1000 ascent, 210/1000 descent, and at 400 it is 98.5% of Arial's width
   for the same string. Overriding the fallback to those numbers means the
   line box and the line breaks are already correct before the font arrives,
   so `swap` changes the lettering and nothing moves. */
@font-face {
  font-family: "Archivo Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Helvetica"), local("Liberation Sans");
  size-adjust: 98.5%;
  ascent-override: 87.8%;
  descent-override: 21%;
  line-gap-override: 0%;
}

[data-theme="dark"] {
  /* Light lettering on a dark ground blooms: the same weight looks heavier and
     the same tracking looks tighter. Compensate on all three axes rather than
     leaving the clubhouse view slightly smeared. A variable font makes the
     weight step a real 20 units instead of a jump to the next static cut. */
  --w-body: 420;
  --w-medium: 520;
  --w-strong: 640;
  --lh-text: 1.6;
  --track-snug: -0.005em;

  --n-0: #0f1218;
  --n-25: #141822;
  --n-50: #171b23;
  --n-100: #1e232d;
  --n-200: #262c37;
  --n-300: #333b48;
  --n-400: #5a6474;
  --n-500: #8a94a4;
  --n-600: #a9b3c1;
  --n-700: #c8d0da;
  --n-800: #e2e7ee;
  --n-900: #f2f5f9;

  --success-bg: #102b21;
  --warning-bg: #2e2312;
  --danger-bg: #2e161c;
  --info-bg: #14203a;

  /* Only the FOREGROUND half is lifted for dark. The grounds above stay as
     they are, because white still has to read on them. */
  --success-text: #2fbe84;
  --warning-text: #d98200;
  --danger-text: #e45468;
  --info-text: #5086f0;
  --captain-gold-text: #d3961f;

  --bg-app: #0f1218;
  --bg-card: #171b23;
  --bg-elevated: #1e232d;
  --bg-sunken: #12161d;
  --text-primary: #eef1f6;
  --text-secondary: #a3adbd;
  --text-tertiary: #818b9a;
  --border-subtle: #242a35;
  --border-default: #2f3745;
  --border-strong: #3d4655;

  --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.45);
  --shadow-md: 0 6px 18px rgba(0, 0, 0, 0.5);
  --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.6);

  /* Lifted for a dark ground; --event-match keeps following the club accent. */
  --event-training: #6aa8ff;
  --event-tournament: #f0a83a;
  --event-meeting: #98a2b3;
  --event-social: #2fbe84;
  --event-fundraiser: #b57cf5;
  --event-tour: #2dd4bf;
  --event-camp: #fb923c;
  --event-other: #98a2b3;

  --accent-text: var(--accent-text-dark);

  color-scheme: dark;
}

/* ---------------------------------------------------------------------------
   2. RESET & BASE
   ------------------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font-ui);
  background: var(--bg-app);
  color: var(--text-primary);
  font-size: var(--fs-body);
  font-weight: var(--w-body);
  line-height: var(--lh-text);
  letter-spacing: var(--track-snug);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior-y: none;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-weight: var(--w-title);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  text-wrap: balance;
}
p { margin: 0; }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; }
/* A canvas that has not been sized by script yet sits at its 300×150 attribute
   default — wider than a small phone. Chart.js loads from a CDN, so until it
   arrives (or when it never does) the bare element must still fit its card
   instead of pushing the whole page sideways. */
canvas { max-width: 100%; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img { max-width: 100%; display: block; }
input, select, textarea { font-family: inherit; }

/* The surfaces nobody draws, which ship with a browser default belonging to no
   design system: the selection wash, the scrollbar, and the tick inside a
   checkbox. Left alone they are the tell that a page was assembled rather than
   built — and on this product they are also a tenancy bug, because a club whose
   colour is navy still got a Chrome-blue checkbox. */
::selection { background: var(--accent-wash-strong); }

/* The standard property as well as the WebKit pseudo-elements: Firefox reads
   only this one, and read neither before, so it drew the platform scrollbar
   against a themed page — conspicuously so in dark mode. */
html { scrollbar-color: var(--n-300) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--n-300); border-radius: var(--r-full); }
::-webkit-scrollbar-thumb:hover { background: var(--n-400); }

/* Checkboxes, radios and range thumbs take the club's colour. --accent-btn
   rather than --accent, because this is a GROUND with a tick on it and the
   button ground is the value solved to carry lettering. */
input[type="checkbox"], input[type="radio"], input[type="range"], progress {
  accent-color: var(--accent-btn);
}
/* A checkbox is a control: it gets a real target and a pointer, not the 13px
   default that a thumb misses next to its own label. */
input[type="checkbox"], input[type="radio"] { width: 18px; height: 18px; cursor: pointer; }

/* Visible, consistent keyboard focus — absent in v1. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* Every figure in the product aligns and never jitters mid-match. */
.text-mono-lg, .text-hero, .text-stat, .figure-value, .score-value,
.stat-card .text-title, .data-table td {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1;
}

/* ---------------------------------------------------------------------------
   3. TYPOGRAPHY
   ------------------------------------------------------------------------ */
/* Five roles, and each is separated from its neighbour on TWO axes at once —
   size and weight, or weight and colour. Title against body is 17/700 against
   15/400: read them side by side with the copy blurred and you can still tell
   which is which. That was not true of the 17/700, 15/650, 14/400, 13/500 the
   scale used to run. */
.text-hero {
  font-family: var(--font-display);
  font-size: var(--fs-hero);
  font-weight: var(--w-display);
  line-height: var(--lh-hero);
  letter-spacing: var(--track-hero);
  text-wrap: balance;
}
.text-title {
  font-size: var(--fs-title); font-weight: var(--w-title);
  line-height: var(--lh-tight); letter-spacing: var(--track-tight);
  color: var(--text-primary);
}
.text-subtitle {
  font-size: var(--fs-body); font-weight: var(--w-strong);
  line-height: var(--lh-snug); letter-spacing: var(--track-snug);
  color: var(--text-secondary);
}
/* max-width on an inline element does nothing, so these caps bind exactly
   where they should: on the paragraph, never on the span inside a row. */
.text-body {
  font-size: var(--fs-body); font-weight: var(--w-body);
  line-height: var(--lh-text); color: var(--text-primary);
  max-width: var(--measure);
}
.text-caption {
  font-size: var(--fs-label); font-weight: var(--w-body);
  line-height: var(--lh-snug); color: var(--text-secondary);
  max-width: var(--measure);
}
/* Present to a screen reader, absent to everything else. The Match Centre's
   staff avatars are the first place that needed it: five initials chips are a
   picture of the coaching team, and the names have to be readable by something
   that cannot see the picture. */
/* THE FILE FIELD. A <label> bound to a visually hidden input — see
   components/file-field.js for why the browser's own button is not used. */
.file-btn {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  min-height: 46px; padding: 0 16px; width: 100%;
  border: 1px dashed var(--border-strong); border-radius: var(--r-sm);
  background: var(--bg-card); color: var(--text-primary);
  font-weight: var(--w-strong); font-size: var(--fs-body);
  cursor: pointer; text-align: center;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.file-btn:hover { background: var(--n-25); border-color: var(--accent); }
/* The input is visually hidden, so the focus ring has to live on the label. */
.sr-only:focus-visible + .file-btn { outline: 2px solid var(--accent); outline-offset: 2px; }
.file-btn.is-chosen { border-style: solid; border-color: var(--success); }
.file-btn-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  border: 0;
}
.text-micro {
  font-size: var(--fs-micro); font-weight: var(--w-title); color: var(--text-tertiary);
  text-transform: uppercase; letter-spacing: var(--track-wide);
  line-height: var(--lh-snug);
}
.text-mono-lg {
  font-family: var(--font-display); font-size: var(--fs-figure);
  font-weight: var(--w-display); line-height: var(--lh-tight);
  letter-spacing: var(--track-hero);
}
/* One step under .figure-value: a KPI tile's number, or the one name a card is
   built to announce. It existed already as `.text-title` plus an inline
   font-size:21px, repeated in six modules — a role with no name. */
.text-stat {
  font-family: var(--font-display); font-size: var(--fs-stat);
  font-weight: var(--w-display); line-height: var(--lh-tight);
  letter-spacing: -0.025em; color: var(--text-primary);
}
.text-success { color: var(--success-text) !important; }
.text-danger { color: var(--danger-text) !important; }
.text-warning { color: var(--warning-text) !important; }
.text-secondary { color: var(--text-secondary) !important; }
.text-brand { color: var(--accent) !important; }
.text-center { text-align: center; }

/* ---------------------------------------------------------------------------
   4. UTILITIES
   ------------------------------------------------------------------------ */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.w-full { width: 100%; }
.relative { position: relative; }
.pointer { cursor: pointer; }
.rounded-full { border-radius: var(--r-full); }

/* NB: gap-6/32, mb-0/20, mt-20 and .mobile-only are all referenced by page
   markup but were never defined in v1 — they silently did nothing (the live
   scoreboard's `gap-32`, for one, collapsed to no gap at all). Defined here. */
.gap-4 { gap: 4px; } .gap-6 { gap: 6px; } .gap-8 { gap: 8px; } .gap-12 { gap: 12px; }
.gap-16 { gap: 16px; } .gap-24 { gap: 24px; } .gap-32 { gap: 32px; }
.mb-0 { margin-bottom: 0; }
.mb-4 { margin-bottom: 4px; } .mb-8 { margin-bottom: 8px; } .mb-12 { margin-bottom: 12px; }
.mb-16 { margin-bottom: 16px; } .mb-20 { margin-bottom: 20px; }
.mb-24 { margin-bottom: 24px; } .mb-32 { margin-bottom: 32px; }
.mt-4 { margin-top: 4px; } .mt-8 { margin-top: 8px; } .mt-12 { margin-top: 12px; }
.mt-16 { margin-top: 16px; } .mt-20 { margin-top: 20px; } .mt-24 { margin-top: 24px; }

.mobile-only { display: none; }
@media (max-width: 980px) { .mobile-only { display: block; } }

.grid { display: grid; gap: 16px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 980px) { .grid-3, .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) {
  /* Content grids collapse; stat grids stay 2-up so a full set stays visible. */
  /* minmax(0, 1fr), NOT a bare 1fr — the two lines above already do this. A
     bare 1fr is minmax(auto, 1fr), so a card holding one long unbreakable word
     widens its own track and pushes the whole page sideways on a phone. */
  .grid-2, .grid-3 { grid-template-columns: minmax(0, 1fr); }
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
}

.divider { height: 1px; background: var(--border-subtle); margin: 20px 0; }
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 22px; gap: 12px; flex-wrap: wrap; }
.section-label { display: flex; align-items: center; justify-content: space-between; margin: 26px 0 12px; gap: 12px; }
.section-label .text-title { font-size: var(--fs-body); }

/* --- The page's own name -------------------------------------------------
   setTopbarTitle() has always been handed the name of the screen and has
   always spent it on document.title alone, so every page rendered without a
   heading: on a phone, where the nav is behind a drawer, nothing on screen
   said where you were, and the largest type on Training was the same 15px as
   its body copy. This is the one element per screen that is allowed to be
   bigger than everything else, and it is also the page's only <h1>. */
.page-title {
  font-family: var(--font-display);
  font-size: var(--fs-page);
  font-weight: var(--w-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-hero);
  color: var(--text-primary);
  margin: 0 0 18px;
}
.page-title:empty { display: none; }
/* On a phone the row directly beneath is usually the page's one action button
   (the top bar has no room for it there), so the title closes up to keep the
   two reading as one header block rather than as two stranded rows. */
@media (max-width: 980px) { .page-title { margin-bottom: 10px; } }

.empty-state { text-align: center; padding: 44px 20px; color: var(--text-secondary); font-size: var(--fs-body); }
.empty-state i { font-size: 30px; color: var(--n-300); margin-bottom: 12px; display: block; }

/* The status glyph on an outcome screen — a registration accepted, a link that
   has expired. It is an icon, not type, so it is sized here rather than from
   the role scale, and at the same 30px as an empty state: the product reports
   an outcome at one size, not at 40px in one place and 48px in another. */
.state-mark { display: block; font-size: 30px; line-height: 1; margin-bottom: 16px; }

/* ---------------------------------------------------------------------------
   5. APP SHELL
   ------------------------------------------------------------------------ */
#app { display: flex; min-height: 100vh; min-height: 100dvh; }
.app-shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.main-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }

/* NO BACKDROP IMAGERY.
   The stadium wallpaper is gone deliberately. Pictures in this product belong
   to content — club crests, faces, match photography — never behind it. A
   photographic wallpaper competes with every surface stacked on top of it and
   forces a veil that greys down the entire interface to stay legible, which is
   exactly the cost that made the old UI feel muddy in daylight. The ground is
   now a flat, honest surface and the imagery has moved into the cards. */

.content {
  padding: 22px 24px 64px;
  max-width: var(--content-max);
  width: 100%;
  margin: 0 auto;
  flex: 1;
}
@media (max-width: 980px) { .content { padding: 16px 14px 40px; } }

/* --- Top bar: the one region the club's colour owns outright -------------- */
.topbar {
  height: calc(var(--topbar-h) + env(safe-area-inset-top));
  padding: env(safe-area-inset-top) 16px 0;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  position: sticky; top: 0; z-index: 50;
  color: var(--on-accent);
  background: var(--accent-btn);
  box-shadow: var(--shadow-sm);
}
[data-theme="dark"] .topbar { background: var(--accent-ink); }
@media (max-width: 560px) { .topbar { padding-left: 10px; padding-right: 10px; } }

.topbar-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
.topbar-right { display: flex; align-items: center; gap: 7px; flex-shrink: 0; }

.icon-btn-light {
  width: 40px; height: 40px; border-radius: var(--r-sm); flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent-veil); color: var(--on-accent); border: none; cursor: pointer;
  font-size: 17px; position: relative;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.icon-btn-light:hover { background: var(--accent-veil-strong); }
.icon-btn-light:active { transform: scale(0.93); }

.topbar-brand { display: flex; align-items: center; gap: 10px; min-width: 0; cursor: pointer; }
.brand-mark {
  width: 38px; height: 38px; border-radius: 10px; flex-shrink: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-veil); border: 1px solid color-mix(in srgb, var(--on-accent) 34%, transparent);
  color: var(--on-accent); font-weight: 900; font-size: 15px;
}
.brand-mark img { width: 100%; height: 100%; object-fit: cover; }
.brand-text { min-width: 0; line-height: 1.15; }
.brand-name {
  font-weight: 800; font-size: 15px; letter-spacing: -0.015em; color: var(--on-accent);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.brand-tag, .org {
  font-size: 11px; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--on-accent); white-space: nowrap;
}
@media (max-width: 560px) {
  .brand-tag { display: none; }
  .brand-name { font-size: 15px; }
}

/* A count, not a dot: "3 waiting" and "1 waiting" are different prompts, and a
   squad announcement is time-critical. Sits on the bell, not beside it. */
.topbar-bell .bell-count {
  position: absolute; top: 3px; right: 2px; min-width: 17px; height: 17px;
  padding: 0 4px; border-radius: var(--r-full);
  display: inline-flex; align-items: center; justify-content: center;
  /* Dark ink on the yellow, not white — white on #ffd23f is ~1.5:1. */
  background: var(--notify); color: var(--ink-shade);
  font-size: 11px; font-weight: 800; line-height: 1;
  font-variant-numeric: tabular-nums lining-nums;
  /* Ringed in the bar's own lettering colour rather than in the bar colour:
     a club with a yellow strip would otherwise have a yellow badge vanish
     into a yellow bar. --on-accent contrasts with the bar by construction. */
  border: 2px solid var(--on-accent);
}
.topbar-bell .bell-count[hidden] { display: none; }

/* --- Notification panel ---------------------------------------------------- */
.notif-panel {
  position: absolute; top: calc(100% + 6px); right: 12px;
  width: min(360px, calc(100vw - 24px));
  background: var(--bg-elevated); color: var(--text-primary);
  border-radius: var(--r-md); border: 1px solid var(--border-subtle);
  box-shadow: var(--shadow-lg);
  z-index: 61; overflow: hidden;
  opacity: 0; transform: translateY(-8px); pointer-events: none;
  transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.notif-panel.open { opacity: 1; transform: translateY(0); pointer-events: auto; }
.notif-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px; border-bottom: 1px solid var(--border-subtle);
}
/* The panel's own heading is a label, not decoration — --text-tertiary is
   3.1:1 in light mode and belongs on marks, not on words. */
.notif-head .text-micro { color: var(--text-secondary); }
.notif-clear {
  background: none; border: none; cursor: pointer; padding: 2px 4px;
  font-size: 13px; font-weight: 700; color: var(--accent-text);
}
.notif-clear:hover { text-decoration: underline; }
.notif-list { max-height: min(60vh, 420px); overflow-y: auto; }
.notif-row {
  display: flex; gap: 12px; width: 100%; text-align: left;
  padding: 12px 14px; background: none; border: none; cursor: pointer;
  border-bottom: 1px solid var(--border-subtle);
  transition: background var(--t-fast) var(--ease);
}
.notif-row:last-child { border-bottom: none; }
.notif-row:hover { background: var(--n-50); }
/* Unread is marked with a dot at the end of the row — the convention people
   already read in every mail app — plus the weight and icon-tint differences
   below. Not a tinted row: a panel of unread items would become a wall of
   colour. */
.notif-row::after {
  content: ''; flex-shrink: 0; align-self: center;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); margin-left: auto;
}
.notif-row.is-read::after { background: transparent; }
.notif-icon {
  width: 34px; height: 34px; flex-shrink: 0; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent-text); font-size: 15px;
}
.notif-row.is-read .notif-icon { background: var(--n-100); color: var(--text-tertiary); }
.notif-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.notif-title { font-size: 15px; font-weight: 700; color: var(--text-primary); }
.notif-row.is-read .notif-title { font-weight: 600; color: var(--text-secondary); }
.notif-body { font-size: 13px; color: var(--text-secondary); }
.notif-when {
  font-size: 11px; font-weight: 600; color: var(--text-secondary);
  font-variant-numeric: tabular-nums lining-nums;
}
.notif-empty {
  padding: 32px 20px; text-align: center;
  font-size: 15px; color: var(--text-secondary);
}
.notif-empty i { display: block; font-size: 26px; color: var(--text-tertiary); margin-bottom: 10px; }
.notif-empty span { font-size: 13px; color: var(--text-secondary); }

.account-chip {
  display: flex; align-items: center; gap: 7px; padding: 4px 9px 4px 4px;
  border-radius: var(--r-full); background: var(--accent-veil);
  border: none; cursor: pointer; color: var(--on-accent); flex-shrink: 0;
  transition: background var(--t-fast) var(--ease);
}
.account-chip:hover { background: var(--accent-veil-strong); }
.account-chip .avatar {
  width: 32px; height: 32px; font-size: 13px;
  border: 1px solid color-mix(in srgb, var(--on-accent) 45%, transparent); background: var(--accent-veil); color: var(--on-accent);
}
.account-chip > i { font-size: 11px; opacity: 0.85; }
@media (max-width: 560px) { .account-chip > i { display: none; } .account-chip { padding: 4px; } }

.account-menu {
  position: absolute; top: calc(100% + 6px); right: 12px; width: 238px;
  background: var(--bg-elevated); color: var(--text-primary);
  border-radius: var(--r-md); border: 1px solid var(--border-subtle);
  box-shadow: var(--shadow-lg);
  padding: 7px; z-index: 60;
  opacity: 0; transform: translateY(-8px); pointer-events: none;
  transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.account-menu.open { opacity: 1; transform: translateY(0); pointer-events: auto; }
.menu-head { padding: 8px 12px 12px; border-bottom: 1px solid var(--border-subtle); margin-bottom: 6px; }
.menu-item {
  display: flex; align-items: center; gap: 12px; padding: 10px 12px;
  border-radius: var(--r-sm); cursor: pointer; font-weight: 600; font-size: 15px;
}
.menu-item:hover { background: var(--n-50); }
.menu-item i { width: 18px; text-align: center; color: var(--text-tertiary); }
.menu-item.danger, .menu-item.danger i { color: var(--danger-text); }

/* Language sits in the account menu rather than only in Settings: a player may
   never open Settings, and someone who has landed in the wrong language has to
   be able to get out of it from wherever they are. Two options shown at once,
   not a toggle — a toggle does not say what it will switch TO. */
.menu-lang { padding: 8px 12px 4px; }
.menu-lang > i { width: 18px; text-align: center; color: var(--text-tertiary); margin-right: 12px; }
.menu-lang > span { font-size: 15px; font-weight: 600; }
.lang-switch {
  display: flex; gap: 4px; margin-top: 8px; padding: 3px;
  border-radius: var(--r-sm); background: var(--n-50);
}
.lang-opt {
  flex: 1; padding: 7px 8px; border: none; border-radius: var(--r-xs); cursor: pointer;
  background: transparent; color: var(--text-secondary);
  font-size: 13px; font-weight: 700; font-family: inherit;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lang-opt:hover { color: var(--text-primary); }
.lang-opt.is-on { background: var(--bg-card); color: var(--text-primary); box-shadow: var(--shadow-xs); }

/* Page actions injected into the coloured bar — inverted so they read on it. */
#topbar-actions { display: flex; align-items: center; gap: 8px; }
#topbar-actions .btn { height: 38px; }
#topbar-actions .btn-primary { background: #fff; color: var(--accent-text-light); box-shadow: var(--shadow-md); }
#topbar-actions .btn-primary:hover { background: #fff; color: var(--accent-text-light); }
#topbar-actions .btn-secondary { background: var(--accent-veil); color: var(--on-accent); border-color: transparent; }
#topbar-actions .btn-secondary:hover { background: var(--accent-veil-strong); }
#topbar-actions .btn-outline { background: transparent; border-color: color-mix(in srgb, var(--on-accent) 55%, transparent); color: var(--on-accent); }
#topbar-actions .btn-outline:hover { border-color: var(--on-accent); }

/* --- Drawer --------------------------------------------------------------- */
.nav-scrim {
  position: fixed; inset: 0; background: rgba(12, 14, 19, 0.5);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  z-index: 300; opacity: 0; pointer-events: none;
  transition: opacity var(--t-med) var(--ease);
}
.nav-scrim.open { opacity: 1; pointer-events: auto; }

.nav-drawer {
  position: fixed; top: 0; left: 0; bottom: 0; width: 282px; max-width: 84vw;
  background: var(--bg-elevated); z-index: 310; box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; padding: 14px 12px;
  padding-top: calc(14px + env(safe-area-inset-top));
  transform: translateX(-105%);
  transition: transform var(--t-med) var(--ease);
  border-right: 1px solid var(--border-subtle);
}
.nav-drawer.open { transform: translateX(0); }
.nav-drawer nav { flex: 1; overflow-y: auto; }
.nav-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px 16px; }
.nav-drawer-brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.nav-drawer-brand .mark {
  width: 36px; height: 36px; border-radius: 10px; overflow: hidden; flex-shrink: 0;
  background: var(--accent-btn);
  display: flex; align-items: center; justify-content: center;
  color: var(--on-accent); font-weight: 800; box-shadow: var(--shadow-accent);
}
.nav-drawer-brand .mark img { width: 100%; height: 100%; object-fit: cover; }
.nav-drawer-brand .name {
  font-weight: 800; font-size: 15px; letter-spacing: -0.02em; color: var(--text-primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nav-drawer-close {
  width: 34px; height: 34px; border-radius: 50%; border: none; cursor: pointer; flex-shrink: 0;
  background: var(--n-50); color: var(--text-secondary);
  display: flex; align-items: center; justify-content: center;
}
.nav-drawer-foot { border-top: 1px solid var(--border-subtle); padding-top: 10px; margin-top: 8px; }

.nav-group { margin-bottom: 8px; }
.nav-item {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 12px; border-radius: var(--r-sm);
  color: var(--text-secondary); font-weight: 600; font-size: 15px;
  cursor: pointer; margin-bottom: 2px; position: relative;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.nav-item i { width: 20px; text-align: center; font-size: 15px; }
.nav-item:hover { background: var(--n-50); color: var(--text-primary); }
/* Accent wash rather than a solid fill: legible against ANY club colour. */
.nav-item.active { background: var(--accent-wash); color: var(--accent); font-weight: 750; }
.nav-item.active i { color: var(--accent); }
[data-theme="dark"] .nav-item:hover { background: var(--n-100); }

/* Legacy sidebar / bottom nav — superseded by the drawer, kept inert. */
.sidebar { display: none; }
.sidebar-logo, .sidebar-footer { padding: 16px; }
.sidebar-logo .mark { width: 36px; height: 36px; border-radius: var(--r-sm); background: var(--accent-btn); color: var(--on-accent); display: flex; align-items: center; justify-content: center; font-weight: 800; }
.sidebar-logo .name { font-weight: 800; font-size: 17px; }
.bottom-nav { display: none; }
.bottom-nav-inner { display: flex; height: var(--bottomnav-h); }
.bottom-nav-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; font-size: 11px; font-weight: 600; color: var(--text-tertiary); }
.bottom-nav-item.active { color: var(--accent); }
.fab-more { width: 46px; height: 46px; border-radius: 50%; background: var(--accent-btn); color: var(--on-accent); display: flex; align-items: center; justify-content: center; }

/* ---------------------------------------------------------------------------
   6. SURFACES
   ------------------------------------------------------------------------ */
.card {
  background: var(--bg-card);
  border-radius: var(--r-md);
  padding: 18px;
  box-shadow: var(--shadow-xs);
  border: 1px solid var(--border-subtle);
  transition: box-shadow var(--t-med) var(--ease), transform var(--t-med) var(--ease), border-color var(--t-med) var(--ease);
}
.card-hover:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--border-default); }
.card-hover:active { transform: translateY(0); }
.card-flat { box-shadow: none; }
.card-pad-lg { padding: 24px; }

/* Same rule as the live bar, and it was broken the same way: --accent-ink is a
   FIXED 72/28 darkening of the club colour rather than a solved one, the sheen
   was a white wash that only ever lightens, and the headline overrode the
   solved ink back to #fff. On a yellow club that measured 1.68:1. Both ends of
   the ramp and the sheen now move away from --on-accent, and nothing on the
   card restates a colour the solver already decided. */
.hero-card {
  border-radius: var(--r-lg);
  background:
    radial-gradient(115% 150% at 100% 0%, color-mix(in srgb, var(--accent-deep) 30%, transparent), transparent 58%),
    linear-gradient(135deg, var(--accent-deep) 0%, var(--accent-btn) 100%);
  color: var(--on-accent);
  padding: 22px;
  position: relative;
  overflow: hidden;
  box-shadow: var(--shadow-md);
}

.stat-card { display: flex; flex-direction: column; gap: 5px; padding: 15px; }
.stat-card .text-title { font-family: var(--font-display); font-size: var(--fs-stat); font-weight: var(--w-display); letter-spacing: -0.025em; line-height: var(--lh-tight); }
.icon-badge {
  width: 36px; height: 36px; border-radius: var(--r-sm);
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 8px; font-size: 15px;
}
@media (max-width: 600px) {
  .stat-card { padding: 13px; gap: 3px; }
  .stat-card .icon-badge { width: 30px; height: 30px; font-size: 13px; margin-bottom: 6px; }
  .stat-card .text-title { font-size: 17px !important; }
  .stat-card .text-micro { font-size: 11px; letter-spacing: 0.04em; }
}

/* ---------------------------------------------------------------------------
   7. BUTTONS
   ------------------------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 44px; padding: 0 18px;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  font-weight: 650; font-size: 15px; letter-spacing: -0.01em;
  cursor: pointer; white-space: nowrap; user-select: none;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}
.btn:active { transform: scale(0.975); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; }
.btn i { font-size: 13px; }

.btn-primary { background: var(--accent-btn); color: var(--on-accent); box-shadow: var(--shadow-accent); }
.btn-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent-btn) 88%, var(--ink-shade)); }
.btn-dark { background: var(--n-800); color: var(--n-0); }
.btn-dark:hover:not(:disabled) { background: var(--n-900); }
.btn-secondary { background: var(--n-50); color: var(--text-primary); border-color: var(--border-default); }
.btn-secondary:hover:not(:disabled) { background: var(--n-100); }
.btn-ghost { background: transparent; color: var(--text-secondary); }
.btn-ghost:hover:not(:disabled) { background: var(--n-50); color: var(--text-primary); }
.btn-outline { background: transparent; border-color: var(--border-default); color: var(--text-primary); }
.btn-outline:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 82%, var(--ink-shade)); }
.btn-success { background: var(--success); color: #fff; }
.btn-success:hover:not(:disabled) { background: color-mix(in srgb, var(--success) 82%, var(--ink-shade)); }

.btn-sm { height: 34px; padding: 0 13px; font-size: 13px; border-radius: var(--r-xs); }
.btn-lg { height: 50px; padding: 0 24px; font-size: 15px; border-radius: var(--r-md); }
.btn-block { width: 100%; display: flex; }
.btn-icon { width: 44px; padding: 0; flex-shrink: 0; }
.btn-icon.btn-sm { width: 34px; }

.fab {
  position: fixed; right: 20px; bottom: calc(22px + env(safe-area-inset-bottom));
  width: 56px; height: 56px; border-radius: 50%;
  background: var(--accent); color: #fff; border: none;
  display: flex; align-items: center; justify-content: center; font-size: 21px;
  box-shadow: var(--shadow-accent); z-index: 150; cursor: pointer;
  transition: transform var(--t-fast) var(--ease-spring);
}
.fab:active { transform: scale(0.92); }

/* ---------------------------------------------------------------------------
   8. BADGES / CHIPS / AVATARS
   ------------------------------------------------------------------------ */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: var(--r-full);
  font-size: 11px; font-weight: 750; letter-spacing: 0.02em;
  text-transform: uppercase; white-space: nowrap;
}
.badge-success { background: var(--success-bg); color: var(--success-text); }
.badge-warning { background: var(--warning-bg); color: var(--warning-text); }
.badge-danger { background: var(--danger-bg); color: var(--danger-text); }
.badge-info { background: var(--info-bg); color: var(--info-text); }
.badge-neutral { background: var(--n-100); color: var(--text-secondary); }

.avatar {
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-weight: 750; letter-spacing: -0.01em;
  background: var(--n-100); color: var(--text-secondary);
  flex-shrink: 0; overflow: hidden;
  width: 40px; height: 40px; font-size: 15px;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: 32px; height: 32px; font-size: 13px; }
.avatar-md { width: 42px; height: 42px; font-size: 15px; }
.avatar-lg { width: 58px; height: 58px; font-size: 17px; }
.avatar-xl { width: 88px; height: 88px; font-size: 26px; }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 34px; padding: 0 14px; border-radius: var(--r-full);
  background: var(--bg-card); color: var(--text-secondary);
  font-weight: 600; font-size: 13px; cursor: pointer;
  border: 1px solid var(--border-default); white-space: nowrap; flex-shrink: 0;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.chip:hover { border-color: var(--border-strong); color: var(--text-primary); }
.chip.active { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 700; }
.chip-scroll { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
/* An author `display` beats the browser's own [hidden] rule, so a chip row
   given the hidden attribute stayed on screen. Every element that sets its own
   display has to say this or `hidden` is decorative. */
.chip-scroll[hidden] { display: none; }
.chip-scroll::-webkit-scrollbar { display: none; }

/* ---------------------------------------------------------------------------
   9. LISTS
   ------------------------------------------------------------------------ */
.list-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 12px; border-radius: var(--r-sm);
  transition: background var(--t-fast) var(--ease);
  min-height: 54px;
}
.list-row:hover { background: var(--n-25); }
[data-theme="dark"] .list-row:hover { background: var(--n-50); }
.list-row + .list-row { margin-top: 1px; }
.list-row-content { flex: 1; min-width: 0; }
.list-row-title { font-weight: 650; font-size: 15px; letter-spacing: -0.01em; }
.list-row-sub { font-size: 13px; color: var(--text-secondary); margin-top: 1px; }
.list-row-end { text-align: right; flex-shrink: 0; margin-left: auto; }
.list-divider { height: 1px; background: var(--border-subtle); margin: 4px 0; }

/* ---------------------------------------------------------------------------
   10. FORMS
   ------------------------------------------------------------------------ */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.field label { font-size: 13px; font-weight: 700; color: var(--text-secondary); }
.field-hint { font-size: 13px; color: var(--text-tertiary); }

.input, .select, textarea.input {
  height: 44px; width: 100%; padding: 0 13px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-default);
  background: var(--bg-card);
  font-size: 15px; color: var(--text-primary);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
textarea.input { height: auto; padding: 11px 13px; min-height: 90px; resize: vertical; line-height: 1.5; }
.input:focus, .select:focus, textarea.input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash);
}
.input::placeholder { color: var(--text-tertiary); }
.input.error { border-color: var(--danger-text); }
.select {
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2397a1b0' stroke-width='1.6' fill='none' fill-rule='evenodd'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center; padding-right: 34px;
}
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 600px) { .form-row { grid-template-columns: 1fr; } }

.search-box { position: relative; }
.search-box i { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); font-size: 15px; pointer-events: none; }
.search-box .input { padding-left: 40px; }

/* ---------------------------------------------------------------------------
   11. TABLES
   ------------------------------------------------------------------------ */
.table-wrap { overflow-x: auto; }
table.data-table { width: 100%; border-collapse: collapse; }
.data-table th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-tertiary); font-weight: 750; padding: 10px 13px;
  border-bottom: 1px solid var(--border-default); white-space: nowrap;
}
.data-table td { padding: 13px; border-bottom: 1px solid var(--border-subtle); font-size: 15px; vertical-align: middle; }
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr { transition: background var(--t-fast) var(--ease); }
.data-table tbody tr:hover { background: var(--n-25); }

/* ---------------------------------------------------------------------------
   12. OVERLAY + SHEET
   Structure is fixed by sheet.js: .sheet is a CHILD of .overlay, `.open` on
   the overlay drives both.
   ------------------------------------------------------------------------ */
.overlay {
  position: fixed; inset: 0; z-index: 900;
  background: rgba(12, 14, 19, 0.55);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  display: flex; align-items: flex-end; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-med) var(--ease);
}
.overlay.open { opacity: 1; pointer-events: auto; }
@media (min-width: 720px) { .overlay { align-items: center; } }

.sheet {
  background: var(--bg-elevated);
  width: 100%; max-width: 560px; max-height: 92vh;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  padding: 8px 22px calc(22px + env(safe-area-inset-bottom));
  overflow-y: auto;
  transform: translateY(32px);
  transition: transform var(--t-med) var(--ease-spring);
  box-shadow: var(--shadow-lg);
}
.overlay.open .sheet { transform: translateY(0); }
@media (min-width: 720px) {
  .sheet { border-radius: var(--r-lg); margin: 20px; padding-bottom: 22px; }
}
.sheet-handle { width: 38px; height: 4px; background: var(--border-strong); border-radius: var(--r-full); margin: 8px auto 16px; }
.sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; gap: 12px; }
.sheet-close {
  width: 34px; height: 34px; border-radius: 50%; background: var(--n-50);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; border: none; color: var(--text-secondary); flex-shrink: 0;
}
.sheet-close:hover { background: var(--n-100); }

/* ---------------------------------------------------------------------------
   13. TOASTS  (store.js toggles `.show`)
   ------------------------------------------------------------------------ */
.toast-container {
  position: fixed; top: calc(14px + env(safe-area-inset-top)); left: 50%;
  transform: translateX(-50%); z-index: 999;
  display: flex; flex-direction: column; gap: 8px; align-items: center;
  width: 100%; padding: 0 16px; pointer-events: none;
}
.toast {
  background: var(--n-800); color: var(--n-0);
  padding: 12px 18px; border-radius: var(--r-sm);
  font-size: 13px; font-weight: 600;
  display: flex; align-items: center; gap: 10px;
  box-shadow: var(--shadow-lg); max-width: 420px;
  opacity: 0; transform: translateY(-14px);
  transition: opacity var(--t-med) var(--ease), transform var(--t-med) var(--ease-spring);
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.success i { color: var(--toast-ok); }
.toast.error i { color: #fca5a5; }

/* ---------------------------------------------------------------------------
   14. LOADING & MOTION
   ------------------------------------------------------------------------ */
.skeleton {
  background: linear-gradient(100deg, var(--n-100) 28%, var(--n-50) 50%, var(--n-100) 72%);
  background-size: 220% 100%;
  animation: skeleton-loading 1.3s ease-in-out infinite;
  border-radius: var(--r-md);
}
@keyframes skeleton-loading { from { background-position: 200% 0; } to { background-position: -200% 0; } }

.spinner, .spinner-dark {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2.5px solid rgba(255, 255, 255, 0.32); border-top-color: #fff;
  animation: spin 0.65s linear infinite; display: inline-block;
}
.spinner-dark { border-color: var(--n-200); border-top-color: var(--accent); }
@keyframes spin { to { transform: rotate(360deg); } }

.fade-in { animation: fadeIn var(--t-med) var(--ease); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* List swaps run inside a view transition (transitionSwap in lib/utils.js):
   items named per record glide to their new slot when a filter narrows the
   list, and the active chip's accent pill glides between chips. One timing
   for the whole product, on the system's deceleration curve. */
::view-transition-group(*) {
  animation-duration: 0.28s;
  animation-timing-function: var(--ease-out);
}

/* First-paint entrance for a list item: a list arriving as a list, top to
   bottom. The item carries --i from its render index; the stagger is capped,
   so item thirteen onwards lands with the twelfth. Filter re-renders must not
   re-apply this class — the view transition owns those. */
.stagger-in {
  animation: fadeIn var(--t-slow) var(--ease-out) both;
  animation-delay: calc(min(var(--i, 0), 12) * 22ms);
}

/* ---------------------------------------------------------------------------
   15. DASHBOARD PRIMITIVES (v2)
   Sectioned lists and figure blocks — the vocabulary that replaces boxing
   everything in a card. Denser, faster to scan, fewer nested edges.
   ------------------------------------------------------------------------ */
.section { margin-bottom: 24px; }
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
/* h2, because a section heading sits directly under the page's <h1>. It was
   an h3, which made every screen jump h1 → h3 and told a screen-reader user
   there was a missing level of structure between them. */
.section-head h2 {
  margin: 0; font-size: var(--fs-micro); font-weight: 750;
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-tertiary);
}
/* The OTHER section heading, for a section that is addressed to the reader
   rather than merely shown to them: what the club is waiting on you for.
   Sentence case at the Title step, so it does not read as one more label in a
   column of labels — the eleven-pixel uppercase eyebrow is correct for "As
   tuas equipas" and wrong for "Precisa de ti", and using it for both is why
   the priority order this product declares was invisible on screen.

   It also had no rule at all: `.section-title` was emitted by two page
   modules and defined in no stylesheet, so the h2 fell to the browser default
   of 1.5em and rendered at 22.5px — off a nine-step ramp whose own No-Half-
   Step Rule forbids exactly that. */
.section-title {
  margin: 0 0 10px; font-size: var(--fs-title); font-weight: var(--w-title);
  letter-spacing: var(--track-tight); color: var(--text-primary);
}
/* --accent is tuned to be a GROUND. As text on the page ground it measured
   3.61:1 for the default club — under AA — and the hit area was 20px tall
   against a 38px floor, so the same element failed contrast and touch at once.
   --accent-text is the club colour solved as a foreground, per theme; the
   inline-flex gives the row a real target while the negative margin keeps the
   lettering optically flush with the section edge.

   It also answers the accent-vs-danger collision: on a red-accent club five
   bright-red links read as five alarms beside a genuine danger row. The
   deepened ink still says "the club's colour" without shouting warning. */
.section-head a {
  font-size: var(--fs-label); font-weight: var(--w-title);
  color: var(--accent-text);
  display: inline-flex; align-items: center;
  min-height: 38px; padding: 0 8px; margin-right: -8px;
  border-radius: var(--r-xs);
}
.section-head a:hover { color: var(--accent); }
.section-body {
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  overflow: hidden;
}
.section-body > .list-row { border-radius: 0; border-bottom: 1px solid var(--border-subtle); margin-top: 0; }
.section-body > .list-row:last-child { border-bottom: none; }

/* Figures — the KPI treatment: a few big earned numbers, the rest secondary.

   A GRID, not a wrapping flex. As a flex row of `min-width: 84px` tiles, four
   figures fit across a 430px phone at 93px each — and then a club that has
   actually collected money renders "15 230,50 €" at 26px inside a 93px box,
   which overruns it and pans the whole page 45px sideways under a scrolling
   thumb. The bug scaled with the club's success and a zero-euro fixture hid
   it. Two-up below 600px is the same step-down .grid-4 already makes, and
   every track is minmax(0, 1fr) so a tile can never be widened by what is
   inside it (The Minmax-Zero Rule). */
.figure-row {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}
@media (max-width: 600px) { .figure-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.figure {
  min-width: 0;
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  padding: 14px 12px; text-align: center;
}
.figure-value {
  font-family: var(--font-display); font-size: var(--fs-figure);
  font-weight: var(--w-display); letter-spacing: var(--track-hero);
  color: var(--text-primary); line-height: var(--lh-tight);
  min-width: 0;
}
/* A money total is the one figure that has no upper bound on length. Past
   roughly nine characters it takes the next step DOWN the ramp rather than
   breaking its tile — a smaller true figure beats a clipped large one. */
.figure-value.is-long { font-size: var(--fs-stat); letter-spacing: var(--track-tight); }
.figure-value.is-longer { font-size: var(--fs-title); letter-spacing: var(--track-tight); }
.figure-label {
  font-size: var(--fs-micro); font-weight: var(--w-title); letter-spacing: var(--track-wide);
  text-transform: uppercase; color: var(--text-tertiary); margin-top: 5px;
}
.figure-hero {
  background: var(--accent-wash);
  border-color: color-mix(in srgb, var(--brand-red) 24%, transparent);
}
.figure-hero .figure-value { color: var(--accent-ink); font-size: var(--fs-match); }
[data-theme="dark"] .figure-hero .figure-value { color: #fff; }
/* --text-tertiary is solved against the CARD and the app ground, not against
   an accent wash: on the hero tile it measured 4.22:1 at 11px in caps. The
   label steps up one level on this ground only — the hierarchy between value
   and label is carried by size and weight here, not by how faint the label
   is. */
.figure-hero .figure-label { color: var(--text-secondary); }

/* "Needs me" — rendered only when something is genuinely wrong. */
.alert-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-radius: var(--r-sm);
  border: 1px solid var(--border-subtle); background: var(--bg-card);
  margin-bottom: 8px; cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.alert-row:hover { border-color: var(--border-strong); transform: translateX(2px); }
.alert-row .alert-icon {
  width: 36px; height: 36px; border-radius: var(--r-sm); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 15px;
}
/* Severity is carried by the tinted icon chip, which is already unmistakable.
   A thick coloured left border on top of that is redundant decoration — and a
   well-worn tell — so the row only tints its own border instead. */
.alert-row.is-danger { border-color: color-mix(in srgb, var(--danger) 32%, var(--border-default)); }
.alert-row.is-warning { border-color: color-mix(in srgb, var(--warning) 32%, var(--border-default)); }
.alert-row.is-info { border-color: color-mix(in srgb, var(--info) 28%, var(--border-default)); }
/* A failure row inside a list section already sits in a .section-body card.
   Standing on its own — the finance failure inside "needs me" — it has no
   container, so it borrows the alert row's chrome and stops looking like text
   that fell out of the layout. */
.list-row.is-error.is-standalone {
  border: 1px solid color-mix(in srgb, var(--danger) 32%, var(--border-default));
  background: var(--bg-card); margin-bottom: 8px; padding: 12px 14px;
}
.list-row.is-error.is-standalone:hover { background: var(--bg-card); }
/* The chip is only sized by `.alert-row .alert-icon`, so a failure row sitting
   beside real alert rows rendered a smaller, differently-aligned icon than its
   neighbours. Same chip, same row. */
.list-row.is-error .alert-icon {
  width: 36px; height: 36px; border-radius: var(--r-sm); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 15px;
}

/* Live match takeover — the product's peak moment, and therefore the LAST
   place a colour may be hard-coded. This ran `--accent-ink` → raw `--brand-red`
   under a fixed `#fff`: white lettering on the club's untouched colour. For the
   default red that measures ~3.9:1; for a yellow or lime club it is about
   1.7:1, illegible, at the top of home, on the one screen the whole club opens
   at once. Both ends of the gradient and every mark on it are solved tokens
   now — see --accent-deep. */
.live-bar {
  display: flex; align-items: center; gap: 14px;
  padding: 16px 18px; border-radius: var(--r-lg);
  background: linear-gradient(120deg, var(--accent-deep), var(--accent-btn));
  color: var(--on-accent); box-shadow: var(--shadow-md); cursor: pointer; margin-bottom: 20px;
}
.live-bar .score-value { font-family: var(--font-display); font-size: var(--fs-score); font-weight: var(--w-display); letter-spacing: var(--track-hero); color: var(--on-accent); }
/* Everything on the bar carries the SOLVED ink at full strength. The label,
   the "vs" and the chevron were dimmed with opacity — 0.75, 0.6, 0.7 — on a
   ground the solver only just clears 4.5:1 on, which put all three between
   2.4:1 and 3.4:1. On this bar hierarchy is size, weight and tracking; it is
   never transparency, because there is no headroom here to spend.
   .text-micro also carries `color: var(--text-tertiary)` — a grey meant for a
   card, not for this ground — so the label must restate the ink explicitly. */
.live-bar .live-label { color: var(--on-accent); }
.live-bar .live-vs { font-weight: var(--w-body); font-size: 0.85em; }
.live-bar .live-chev { flex-shrink: 0; }
.live-dot {
  width: 9px; height: 9px; border-radius: 50%; background: var(--on-accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--on-accent) 25%, transparent);
  animation: live-pulse 1.6s ease-in-out infinite; flex-shrink: 0;
}
@keyframes live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* Sponsor slot — dignified, collapses cleanly when nothing is sold, and never
   placed inside the "needs me" region. Brand-agnostic by construction. */
.sponsor-slot {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  padding: 14px; margin-top: 20px;
  border-top: 1px solid var(--border-subtle);
}
.sponsor-slot:empty { display: none; }
.sponsor-label {
  font-size: 11px; font-weight: 750; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--text-tertiary);
}
.sponsor-logo { max-height: 28px; width: auto; opacity: 0.92; }

/* ---------------------------------------------------------------------------
   16. ACCESSIBILITY
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    /* Delays too: a staggered entrance with `both` fill keeps content
       INVISIBLE for the length of its delay even after the duration is
       zeroed. Reduced motion means the content is simply there. */
    animation-delay: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* Comfortable touch targets on phones (pitchside, one-handed). */
@media (pointer: coarse) {
  .btn-sm { height: 38px; }
  .chip { height: 38px; }
  /* Dismiss is the one control every sheet has, and it sat at 34px — under
     this system's own compact floor — on every sheet in the product. */
  .sheet-close { width: 38px; height: 38px; }
  .btn-icon.btn-sm { width: 38px; }
}


/* ---------------------------------------------------------------------------
   17. MATCHDAY — the signature surface
   Modelled on how rugby media presents a fixture (RugbyPass): both sides given
   equal weight around a central scoreline, identity carried by a crest, and the
   state of the match — LIVE, FT, or kickoff time — stated once and plainly.
   Pictures belong to content here: a real club crest where one exists, and an
   honest monogram where none does. We never invent an opponent's badge.
   ------------------------------------------------------------------------ */
.match-card {
  display: block;
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  overflow: hidden;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.match-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-sm); }

/* The whole card navigates, but the card is not the control: one stretched
   link covers it, so the keyboard gets a real target and the venue link and
   action buttons stay reachable above it rather than being trapped inside a
   button. The card owns the focus ring on the link's behalf. */
.match-card.is-linked { position: relative; }
.match-open { position: absolute; inset: 0; z-index: 1; }
.match-card:has(.match-open:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.match-open:focus-visible { outline: none; }
.match-meta a, .match-actions { position: relative; z-index: 2; }

/* Competition strip: the one line of context above the fixture itself. */
.match-comp {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 9px 14px;
  background: var(--bg-sunken);
  border-bottom: 1px solid var(--border-subtle);
  font-size: 11px; font-weight: 750; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-tertiary);
}

/* The fixture row: side — score — side, both sides weighted equally. */
.match-body {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  padding: 16px 14px;
}
.match-side { display: flex; align-items: center; gap: 10px; min-width: 0; }
.match-side.is-away { flex-direction: row-reverse; text-align: right; }
.match-team {
  font-family: var(--font-display);
  font-size: var(--fs-body); font-weight: var(--w-title); letter-spacing: var(--track-tight);
  color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.match-score {
  font-family: var(--font-display);
  font-size: var(--fs-match); font-weight: var(--w-display); letter-spacing: -0.04em;
  color: var(--text-primary); line-height: 1;
  font-variant-numeric: tabular-nums lining-nums;
  display: flex; align-items: baseline; gap: 7px;
}
.match-score .sep { color: var(--text-tertiary); font-weight: 700; }
.match-score .is-losing { color: var(--text-tertiary); }

/* Kickoff time stands in for the score before a match is played. */
.match-kickoff {
  font-family: var(--font-display);
  font-size: var(--fs-title); font-weight: var(--w-title); letter-spacing: var(--track-tight);
  color: var(--text-primary); text-align: center; line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
}
/* The date that used to sit under this as a <small> has gone — the
   competition strip states it once. With the column down to one line the time
   takes the weight the pair was splitting: it is the single thing an upcoming
   fixture is opened to find. */
.match-kickoff { font-size: var(--fs-stat); font-weight: var(--w-display); }

.match-meta {
  padding: 0 14px 13px;
  font-size: var(--fs-label); color: var(--text-secondary);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
/* The venue opens directions in a map — a real destination, and on matchday
   the one link a coach taps with a thumb. Inline it measured 231×20, so it
   gets a genuine target: the row is already a flex line, so an inline-flex
   with a 38px min-height grows the hit area without moving the lettering. */
.match-meta a {
  color: var(--accent-text); font-weight: 650;
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 38px;
  border-radius: var(--r-xs);
}

/* Status: one word, stated once. */
.match-status { display: inline-flex; align-items: center; gap: 5px; font-weight: 800;
  font-variant-numeric: tabular-nums lining-nums; }   /* it prints a date */
.match-status.is-live { color: var(--accent); }
.match-status.is-ft { color: var(--text-tertiary); }

/* --- Team identity -------------------------------------------------------
   A crest when the club has uploaded one; otherwise a monogram built from the
   name and tinted deterministically, so an opponent is recognisable without a
   fabricated badge. */
.team-badge {
  width: 40px; height: 40px; border-radius: var(--r-full);
  flex-shrink: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: var(--n-100);
  border: 1px solid var(--border-subtle);
}
.team-badge img { width: 100%; height: 100%; object-fit: contain; padding: 3px; }
/* Our own teams wear a crest that says what kind of rugby it is; the club's
   colour underneath says which team. Same ground and the same solved ink as
   the monogram it replaces, so a club that picks a pale green does not get
   white-on-pale. The glyph is a shade smaller than the lettering was — an icon
   fills its box where two letters do not. */
.team-badge.is-icon {
  color: var(--badge-ink, #fff);
  border-color: transparent;
  font-size: 17px;
}
.team-badge.is-icon.team-badge-sm { font-size: 13px; }
.team-badge.is-icon.mc-crest { font-size: 21px; }

.team-badge.is-monogram {
  font-family: var(--font-display);
  font-size: 15px; font-weight: 800; letter-spacing: -0.02em;
  /* The ink is decided per team by teamBadgeHtml(), because the ground is a
     colour the CLUB chose. A hard-coded #fff measured 3.33:1 on the default
     blue and 3.35:1 on the green, in BOTH themes — the one contrast failure
     in this system that no amount of theming could fix, because it was never
     a theme problem. The fallback stays white for a badge rendered without
     the solved value. */
  color: var(--badge-ink, #fff);
  border-color: transparent;
}
.team-badge-lg { width: 56px; height: 56px; }
.team-badge-lg.is-monogram { font-size: 21px; }   /* the `stat` step — no bespoke size for one component */
.team-badge-sm { width: 30px; height: 30px; }
.team-badge-sm.is-monogram { font-size: 11px; }

/* --- Editorial thumbnail -------------------------------------------------
   Content imagery: a real photo at a fixed ratio, never a decorative texture. */
.thumb {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  background: var(--n-100); overflow: hidden;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; }
.thumb-sq { aspect-ratio: 1 / 1; }
/* Scrim only where text actually sits over the image. */
.thumb-scrim {
  position: absolute; inset: auto 0 0 0; padding: 14px;
  background: linear-gradient(to top, rgba(8, 10, 14, 0.82), rgba(8, 10, 14, 0.35) 55%, transparent);
  color: #fff;
}
.thumb-scrim .text-hero, .thumb-scrim .match-team { color: #fff; }

/* --- Player tile: the face leads --------------------------------------- */
.player-tile {
  background: var(--bg-card); border: 1px solid var(--border-subtle);
  border-radius: var(--r-md); overflow: hidden; cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.player-tile:hover { border-color: var(--border-strong); transform: translateY(-2px); }
.player-tile-body { padding: 11px 12px 13px; }
.player-tile-name {
  font-family: var(--font-display);
  font-size: var(--fs-body); font-weight: var(--w-title); letter-spacing: var(--track-tight); color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

@media (max-width: 420px) {
  .match-body { padding: 14px 10px; gap: 6px; }
  .match-score { font-size: var(--fs-figure); }
  .team-badge { width: 34px; height: 34px; }
}

/* Fixture actions sit below the scoreline so they never compete with it. */
.match-actions {
  display: flex; align-items: center; gap: 8px;
  padding: 0 12px 12px;
  border-top: 1px solid var(--border-subtle);
  padding-top: 12px; margin-top: 2px;
}
.match-actions .btn-icon { margin-left: auto; }
.match-actions .btn-icon + .btn-icon { margin-left: 0; }

/* Fixtures stack full-width on phones and pair up on wider screens; a match
   card needs its horizontal room for two names plus a scoreline. */
.match-list { display: grid; gap: 12px; grid-template-columns: 1fr; }
@media (min-width: 900px) { .match-list { grid-template-columns: 1fr 1fr; } }

/* Rows and tiles that became real <button>s for keyboard access. They must look
   exactly as they did as divs — the change is semantic, not visual. */
.player-pick-item, button.list-row, button.chip, .pitch-spot, .bench-slot {
  font: inherit; color: inherit; text-align: left; appearance: none; -webkit-appearance: none;
}
.player-pick-item { display: flex; align-items: center; gap: 12px; width: 100%;
  background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
  padding: 9px 10px; cursor: pointer; }
.player-pick-item:hover:not(.disabled) { background: var(--n-25); }
.player-pick-item.disabled { opacity: 0.55; cursor: not-allowed; }
.player-pick-item.selected { background: var(--accent-wash); border-color: color-mix(in srgb, var(--brand-red) 26%, transparent); }
button.list-row { width: 100%; background: transparent; border: 0; cursor: pointer; }

/* A whole surface that navigates — a fixture card, an alert row, a team card.
   It is an <a href> to a hash route, so the browser gives keyboard access, the
   focus ring, middle-click and "open in new tab" for nothing; this only strips
   the link's own text styling so the surface looks exactly as it did when it
   was a div with a click handler. Blocks, so it can wrap flow content. */
.nav-block {
  display: block;
  cursor: pointer;
}
/* NO `color: inherit` here. The global `a` rule above already sets it, at
   lower specificity — re-declaring it on .nav-block (0,1,0) beat
   `.live-bar { color: #fff }` (0,1,0, but earlier in source) and painted the
   live bar's team names in near-black ink on the red gradient: 3.17:1, on the
   product's loudest surface. A surface that states its own colour must keep
   it; a plain row that states none still inherits correctly from `a`. */
/* The ring hugs the surface's own corner radius instead of boxing it. */
.nav-block:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-md); }
.live-bar.nav-block:focus-visible, .next-fixture.nav-block:focus-visible { border-radius: var(--r-lg); }
.list-row.nav-block:focus-visible { border-radius: var(--r-sm); }
/* The rows already lay themselves out as flex; `display: block` above would
   undo that, so restore what each surface actually is. */
.list-row.nav-block, .alert-row.nav-block, .live-bar.nav-block { display: flex; }

/* --- THE SECOND RANK OF FIGURES -------------------------------------------
   Numbers that belong to a record but do not describe it: conversions, drop
   goals, cards. They were being set as stat tiles identical to the four
   figures that DO describe a season, on two different surfaces, so a nil at
   drop goals carried the weight of six hundred minutes played.
   Reading-size numbers, micro labels, one sunken strip. Wraps rather than
   scrolls — six short pairs are a paragraph of numbers, not a carousel. */
.fig-row {
  display: flex; flex-wrap: wrap; gap: 10px 28px;
  padding: 14px 16px; border-radius: var(--r-md);
  background: var(--bg-sunken);
}
.fig { min-width: 0; }
.fig-n {
  display: block; font-size: 17px; font-weight: 700;
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -0.02em; color: var(--text-primary); line-height: 1.2;
}
/* Worn only by a non-zero value — see detailFiguresHtml(). */
.fig-n.is-captain { color: var(--captain-gold-text); }
.fig-n.is-warning { color: var(--warning-text); }
.fig-n.is-danger { color: var(--danger-text); }
.fig-l {
  display: block; margin-top: 1px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary);
}

/* --- IMPERSONATION BANNER --------------------------------------------------
   An admin looking at the app through a family's session sees, by design, a
   screen indistinguishable from that family's. This bar is the only thing
   telling them otherwise, so it is fixed, full width, above every other layer,
   and has no dismiss. Ink rather than danger red: this is a state, not a fault,
   and a red bar across the top of every screen for an hour trains the eye to
   stop seeing it. */
.imp-bar {
  position: fixed; inset: 0 0 auto 0; z-index: 1000;
  display: flex; align-items: center; gap: 12px;
  min-height: 44px; padding: 8px 16px;
  background: var(--text-primary); color: var(--bg-card);
  font-size: 13px; font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.imp-bar > i { flex: none; opacity: 0.8; }
.imp-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.imp-exit {
  margin-left: auto; flex: none;
  min-height: 32px; padding: 0 14px;
  border-radius: var(--r-full); border: 1px solid rgba(255, 255, 255, 0.35);
  background: transparent; color: inherit; font: inherit; font-weight: 700;
  cursor: pointer;
}
.imp-exit:hover { background: rgba(255, 255, 255, 0.14); }
.imp-exit:focus-visible { outline: 2px solid var(--bg-card); outline-offset: 2px; }
.imp-exit[disabled] { opacity: 0.6; cursor: default; }
/* The bar is fixed, so the shell has to be pushed out from under it — including
   the sticky top bar, which would otherwise sit behind it. */
.app-shell.is-impersonating { padding-top: 44px; }
.app-shell.is-impersonating .topbar { top: 44px; }
